JAVA多線程之深入研究 Java Synchronize 和 Lock 的區別與用法

來源:互聯網
上載者:User

標籤:java se   gre   string   獲得   基本   這一   lib   obj   throwable   

在分布式開發中,鎖是線程式控制制的重要途徑。Java為此也提供了2種鎖機制,synchronized和lock。做為Java愛好者,自然少不了對比一下這2種機制,也能從中學到些分布式開發需要注意的地方。
 
我們先從最簡單的入手,逐步分析這2種的區別。
 
一、synchronized和lock的用法區別
 
synchronized:在需要同步的對象中加入此控制,synchronized可以加在方法上,也可以加在特定代碼塊中,括弧中表示需要鎖的對象。
 
lock:需要顯示指定起始位置和終止位置。一般使用ReentrantLock類做為鎖,多個線程中必須要使用一個ReentrantLock類做為對象才能保證鎖的生效。且在加鎖和解鎖處需要通過lock()和unlock()顯示指出。所以一般會在finally塊中寫unlock()以防死結。
 
用法區別比較簡單,這裡不贅述了,如果不懂的可以看看Java基本文法。
 
二、synchronized和lock效能區別
 
synchronized是託管給JVM執行的,而lock是java寫的控制鎖的代碼。在Java1.5中,synchronize是效能低效的。因為這是一個重量級操作,需要叫用作業介面,導致有可能加鎖消耗的系統時間比加鎖以外的操作還多。相比之下使用Java提供的Lock對象,效能更高一些。但是到了Java1.6,發生了變化。synchronize在語義上很清晰,可以進行很多最佳化,有適應自旋,鎖消除,鎖粗化,輕量級鎖,偏向鎖等等。導致在Java1.6上synchronize的效能並不比Lock差。官方也表示,他們也更支援synchronize,在未來的版本中還有最佳化餘地。
 
說到這裡,還是想提一下這2中機制的具體區別。據我所知,synchronized原始採用的是CPU悲觀鎖機制,即線程獲得的是獨佔鎖。獨佔鎖意味著其他線程只能依靠阻塞來等待線程釋放鎖。而在CPU轉換線程阻塞時會引起線程環境切換,當有很多線程競爭鎖的時候,會引起CPU頻繁的環境切換導致效率很低。
 
而Lock用的是樂觀鎖方式。所謂樂觀鎖就是,每次不加鎖而是假設沒有衝突而去完成某項操作,如果因為衝突失敗就重試,直到成功為止。樂觀鎖實現的機制就是CAS操作(Compare and Swap)。我們可以進一步研究ReentrantLock的原始碼,會發現其中比較重要的獲得鎖的一個方法是compareAndSetState。這裡其實就是調用的CPU提供的特殊指令。
 
現代的CPU提供了指令,可以自動更新共用資料,而且能夠檢測到其他線程的幹擾,而 compareAndSet() 就用這些代替了鎖定。這個演算法稱作非阻塞演算法,意思是一個線程的失敗或者掛起不應該影響其他線程的失敗或掛起的演算法。
 
我也只是瞭解到這一步,具體到CPU的演算法如果感興趣的讀者還可以在查閱下,如果有更好的解釋也可以給我留言,我也學習下。
 
三、synchronized和lock用途區別
 
synchronized原語和ReentrantLock在一般情況下沒有什麼區別,但是在非常複雜的同步應用中,請考慮使用ReentrantLock,特別是遇到下面2種需求的時候。
 
1.某個線程在等待一個鎖的控制權的這段時間需要中斷
2.需要分開處理一些wait-notify,ReentrantLock裡面的Condition應用,能夠控制notify哪個線程
3.具有公平鎖功能,每個到來的線程都將排隊等候
 
下面細細道來……
 
先說第一種情況,ReentrantLock的lock機制有2種,忽略中斷鎖和響應中斷鎖,這給我們帶來了很大的靈活性。比如:如果A、B2個線程去競爭鎖,A線程得到了鎖,B線程等待,但是A線程這個時候實在有太多事情要處理,就是一直不返回,B線程可能就會等不及了,想中斷自己,不再等待這個鎖了,轉而處理其他事情。這個時候ReentrantLock就提供了2種機制,第一,B線程中斷自己(或者別的線程中斷它),但是ReentrantLock不去響應,繼續讓B線程等待,你再怎麼中斷,我全當耳邊風(synchronized原語就是如此);第二,B線程中斷自己(或者別的線程中斷它),ReentrantLock處理了這個中斷,並且不再等待這個鎖的到來,完全放棄。(如果你沒有瞭解java的中斷機制,請參考下相關資料,再回頭看這篇文章,80%的人根本沒有真正理解什麼是java的中斷,呵呵)
 
這裡來做個實驗,首先搞一個Buffer類,它有讀操作和寫操作,為了不讀到髒資料,寫和讀都需要加鎖,我們先用synchronized原語來加鎖,如下:

 
public class Buffer {       private Object lock;       public Buffer() {        lock = this;    }       public void write() {        synchronized (lock) {            long startTime = System.currentTimeMillis();            System.out.println("開始往這個buff寫入資料…");            for (;;)// 類比要處理很長時間            {                if (System.currentTimeMillis()                        - startTime > Integer.MAX_VALUE)                    break;            }            System.out.println("終於寫完了");        }    }       public void read() {   synchronized (lock) {            System.out.println("從這個buff讀資料");        }    }}

接著,我們來定義2個線程,一個線程去寫,一個線程去讀。

public class Writer extends Thread {       private Buffer buff;       public Writer(Buffer buff) {        this.buff = buff;    }       @Override    public void run() {   buff.write();    }   }   public class Reader extends Thread {       private Buffer buff;       public Reader(Buffer buff) {        this.buff = buff;    }       @Override    public void run() {           buff.read();//這裡估計會一直阻塞           System.out.println("讀結束");       }   }

 

好了,寫一個Main來實驗下,我們有意先去“寫”,然後讓“讀”等待,“寫”的時間是無窮的,就看“讀”能不能放棄了。

public class Test {    public static void main(String[] args) {        Buffer buff = new Buffer();           final Writer writer = new Writer(buff);        final Reader reader = new Reader(buff);           writer.start();        reader.start();           new Thread(new Runnable() {               @Override            public void run() {                long start = System.currentTimeMillis();                for (;;) {                    //等5秒鐘去中斷讀                    if (System.currentTimeMillis()                            - start > 5000) {                        System.out.println("不等了,嘗試中斷");                        reader.interrupt();                        break;                    }                   }               }        }).start();       }}

 

我們期待“讀”這個線程能退出等待鎖,可是事與願違,一旦讀這個線程發現自己得不到鎖,就一直開始等待了,就算它等死,也得不到鎖,因為寫線程要21億秒才能完成 T_T ,即使我們中斷它,它都不來響應下,看來真的要等死了。這個時候,ReentrantLock給了一種機制讓我們來響應中斷,讓“讀”能伸能屈,勇敢放棄對這個鎖的等待。我們來改寫Buffer這個類,就叫BufferInterruptibly吧,可中斷緩衝。

 

當然,要對reader和writer做響應的修改

public class Reader extends Thread {       private BufferInterruptibly buff;       public Reader(BufferInterruptibly buff) {        this.buff = buff;    }       @Override    public void run() {           try {            buff.read();//可以收到中斷的異常,從而有效退出        } catch (InterruptedException e) {            System.out.println("我不讀了");        }           System.out.println("讀結束");       }   }   /*** Writer倒不用怎麼改動*/public class Writer extends Thread {       private BufferInterruptibly buff;       public Writer(BufferInterruptibly buff) {        this.buff = buff;    }       @Override    public void run() {        buff.write();    }   }   public class Test {    public static void main(String[] args) {        BufferInterruptibly buff = new BufferInterruptibly();           final Writer writer = new Writer(buff);        final Reader reader = new Reader(buff);           writer.start();        reader.start();           new Thread(new Runnable() {               @Override            public void run() {                long start = System.currentTimeMillis();                for (;;) {                    if (System.currentTimeMillis()                            - start > 5000) {                        System.out.println("不等了,嘗試中斷");                        reader.interrupt();                        break;                    }                   }               }        }).start();       }}

 

這次“讀”線程接收到了lock.lockInterruptibly()中斷,並且有效處理了這個“異常”。

 

 

 

至於第二種情況,ReentrantLock可以與Condition的配合使用,Condition為ReentrantLock鎖的等待和釋放提供控制邏輯。
 
例如,使用ReentrantLock加鎖之後,可以通過它自身的Condition.await()方法釋放該鎖,線程在此等待Condition.signal()方法,然後繼續執行下去。await方法需要放在while迴圈中,因此,在不同線程之間實現並發控制,還需要一個volatile的變數,boolean是原子性的變數。因此,一般的並發控制的操作邏輯如下所示:

volatile boolean isProcess = false;ReentrantLock lock  = new ReentrantLock();Condtion processReady = lock.newCondtion();thread: run() {    lock.lock();    isProcess = true;   try {    while(!isProcessReady) {  //isProcessReady 是另外一個線程的控制變數      processReady.await();//釋放了lock,在此等待signal     }catch (InterruptedException e) {          Thread.currentThread().interrupt();        } finally {          lock.unlock();          isProcess = false;        }      }    }}

 

這裡只是代碼使用的一段簡化,下面我們看Hadoop的一段摘取的源碼:

private class MapOutputBuffer<K extends Object, V extends Object>      implements MapOutputCollector<K, V>, IndexedSortable {...    boolean spillInProgress;    final ReentrantLock spillLock = new ReentrantLock();    final Condition spillDone = spillLock.newCondition();    final Condition spillReady = spillLock.newCondition();    volatile boolean spillThreadRunning = false;    final SpillThread spillThread = new SpillThread();...    public MapOutputBuffer(TaskUmbilicalProtocol umbilical, JobConf job,                           TaskReporter reporter                           ) throws IOException, ClassNotFoundException {    ...      spillInProgress = false;      spillThread.setDaemon(true);      spillThread.setName("SpillThread");      spillLock.lock();      try {        spillThread.start();        while (!spillThreadRunning) {          spillDone.await();        }      } catch (InterruptedException e) {        throw new IOException("Spill thread failed to initialize", e);      } finally {        spillLock.unlock();      }    }    protected class SpillThread extends Thread {      @Override      public void run() {        spillLock.lock();        spillThreadRunning = true;        try {          while (true) {            spillDone.signal();            while (!spillInProgress) {              spillReady.await();            }            try {              spillLock.unlock();              sortAndSpill();            } catch (Throwable t) {              sortSpillException = t;            } finally {              spillLock.lock();              if (bufend < bufstart) {                bufvoid = kvbuffer.length;              }              kvstart = kvend;              bufstart = bufend;              spillInProgress = false;            }          }        } catch (InterruptedException e) {          Thread.currentThread().interrupt();        } finally {          spillLock.unlock();          spillThreadRunning = false;        }      }    }

代碼中spillDone 就是 spillLock的一個newCondition()。調用spillDone.await()時可以釋放spillLock鎖,線程進入阻塞狀態,而等待其他線程的 spillDone.signal()操作時,就會喚醒線程,重新持有spillLock鎖。
 
這裡可以看出,利用lock可以使我們多線程互動變得方便,而使用synchronized則無法做到這點。
 
最後呢,ReentrantLock這個類還提供了2種競爭鎖的機制:公平鎖和非公平鎖。這2種機制的意思從字面上也能瞭解個大概:即對於多線程來說,公平鎖會依賴線程進來的順序,後進來的線程後獲得鎖。而非公平鎖的意思就是後進來的鎖也可以和前邊等待鎖的線程同時競爭鎖資源。對於效率來講,當然是非公平鎖效率更高,因為公平鎖還要判斷是不是線程隊列的第一個才會讓線程獲得鎖。

 

原文地址:http://blog.csdn.net/natian306/article/details/18504111

JAVA多線程之深入研究 Java Synchronize 和 Lock 的區別與用法

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.