java併發程式設計之wait¬ify VS lock&am
jdk5之前執行緒同步可以用synchronized/wait/notify來進行控制,jdk5以後新新增了lock/condition。他們之間有什麼聯絡與區別的?本文就用一個例子循序漸進的給大家展示一下:
首先來看一個有界快取的例子:
abstract class BaseBoundedBuffer{ private final V[] buff; private int tail; private int head; private int count; protected BaseBoundedBuffer(int capacity){ this.buff = (V[])new Object[capacity]; } protected synchronized final void doPut(V v){//存 buff[tail] = v; tail++; if(tail == buff.length){ tail = 0; } count++; } protected synchronized final V doTake(){//get V v = buff[head]; buff[head] = null; head++; if(head == buff.length){ head = 0; } count--; return v; } protected synchronized final boolean isFull(){//是否是滿的 return count == buff.length; } protected synchronized final boolean isEmpty(){//是否是空的 return count == 0; } }
class GrumpBoundedBuferextends BaseBoundedBuffer { public GrumpBoundedBufer(int size){ super(size); } public synchronized void put(V v)throws BufferFullException{ if(isFull()){//存的時候,如果是滿的,就拋異常 throw new BufferFullException(); } doPut(v); } public synchronized V take()throws BufferEmptyException{ if(isEmpty()){//取的時候,如果是空的,就拋異常 throw new BufferEmptyException(); } return doTake(); } }
當然,上面的這種實現非常不友好,如果不滿足先驗條件就丟擲異常,但是在多執行緒條件下,先驗條件不會保持一個一成不變的狀態,佇列裡面的元素是在不停的變化的,因此我們用輪詢加休眠改進一下:
class SleepyBoundedBuferextends BaseBoundedBuffer { public SleepyBoundedBufer(int size){ super(size); } public void put(V v) throws InterruptedException { while(true){ synchronized(this){ if(!isFull()){//如果不是滿的,可以存 doPut(v); return; } } //如果是滿的,休眠1秒鐘,然後重試 Thread.sleep(1000); } } public V take() throws InterruptedException { while(true){ synchronized(this){ if(!isEmpty()){//如果不是空的,就可以取 return doTake(); } } //如果是空的,休眠1秒鐘,重試 Thread.sleep(1000); } } }
這種輪訓+休眠的方式的缺點:
(1)休眠多少時間合適呢?
(2)給呼叫者提出處理InterruptedException的新的要求,因為sleep是會丟擲這個異常的。
如果存在一種執行緒掛起的方式,它能保證,在某個條件變為真的時候,執行緒可以及時的甦醒過來,那就太好了!這就是條件佇列所做的事情。
使用內部條件佇列的實現方式:
class BoundedBuferextends BaseBoundedBuffer { protected BoundedBufer(int size) { super(size); } public synchronized void put(V v) throws InterruptedException { while(isFull()){//注意這裡的while,而不是if wait();//如果是滿的,把當前執行緒掛起 } doPut(v);//如果不滿,就可以存 notifyAll();//存了以後,喚醒所有的等待執行緒,因為可能有執行緒在等待取,放進來以後就可以取了 } public synchronized V take() throws InterruptedException { while(isEmpty()){//注意這裡的while,而不是if wait();//如果是空的,把當前執行緒掛起 } V v = doTake();//如果不空,取出來 notifyAll();//然後喚醒所有的等待執行緒,因為有的執行緒可能在等待放,取出來以後就可以放了 return v; } }
這也是jdk5之前的解決方式。
條件佇列可以讓一組執行緒(叫做:等待集wait set)以某種方式等待相關條件變為真,條件佇列的元素不同於一般的佇列,一般佇列的元素是資料項,條件佇列的元素是執行緒。每個java物件都有一個內部鎖,同時還有一個內部條件佇列。一個物件的內部鎖和內部條件佇列是關聯在一塊的。Object.wait會自動釋放鎖,並請求os掛起當前執行緒,這樣就給其他執行緒獲得鎖並修改物件狀態的機會,當執行緒被喚醒以後,它會重新去獲取鎖。呼叫wait以後,執行緒就進入了物件的內部條件佇列裡面等待,呼叫notify以後,就從物件的內部條件佇列裡面選擇一個等待執行緒,喚醒。 因為會有多個執行緒因為不同的原因在同一個條件佇列中等待,因此,用notify而不用notifyAll是危險的!有的執行緒是在take()的時候阻塞,它等待的條件是佇列不空,有的執行緒是在put()的時候阻塞,它等待的條件是佇列非滿。 如果呼叫了take()以後notify的是總是阻塞在take上的執行緒,就掛了!
BoundedBufer的put和take是一種很保守的做法,每次向佇列裡面新增或者移除都進行notifyAll,可以進行如下的最佳化:
是有從空變為了非空,或者是從滿變為了不滿的時候,才需要從條件佇列裡面喚醒一個執行緒。
class ConditionalBoundedBuferextends BaseBoundedBuffer { protected ConditionalBoundedBufer(int size) { super(size); } public synchronized void put(V v) throws InterruptedException { while(isFull()){ wait(); } boolean isEmpty = isEmpty(); doPut(v); if(isEmpty){//從空變為了非空的時候,才需要喚醒(而實際上需要喚醒那些take執行緒,而不是put執行緒) notifyAll(); } } public synchronized V take() throws InterruptedException { while(isEmpty()){ wait(); } boolean isFull = isFull(); V v = doTake(); if(isFull){//從滿變為了不滿,才需要喚醒(而實際上需要喚醒那些put執行緒,而不是take執行緒) notifyAll(); } return v; } }
這只是一種小技巧,會加大程式的複雜性,不提倡!
從空變為了非空,喚醒的應該是那些阻塞在take()上的,從滿變為了不滿喚醒的應該是那些阻塞在put()上的執行緒,而notifyAll會把所有條件佇列裡面的所有的等待的執行緒全部喚醒,這就顯現出了內部條件佇列有一個缺陷:內部鎖只能有一個與之關聯的條件佇列。顯式的condition的出現就是為了解決這個問題。
正如Lock提供了比內部鎖更豐富的特徵一樣,condition也提供了比內部條件佇列更豐富更靈活的功能。一個lock可以有多個condition,一個condition只關聯到一個Lock。
class ConditionBoundedBufer{//使用顯式的條件變數,HLL的登場了 private Lock lock = new ReentrantLock(); private Condition notEmpty = lock.newCondition(); private Condition notFull = lock.newCondition(); private final T[] items = (T[])new Object[100]; private int head,tail,count; //阻塞,一直到notFull public void put(T t) throws InterruptedException { lock.lock(); try{ while(count == items.length){ notFull.await();//等待非滿 } items[tail] = t; tail ++; if(tail == items.length){ tail = 0; } count++; notEmpty.signal();//喚醒那些執行take()而阻塞的執行緒 }finally{ lock.unlock(); } } //阻塞,一直到notEmpty public T take() throws InterruptedException { lock.lock(); try{ while(count == 0){ notEmpty.await();//等待非空 } T t = items[head]; items[head] = null; head ++; if(head == items.length){ head = 0; } count--; notFull.signal();//喚醒那些執行put()而阻塞的執行緒 return t; }finally{ lock.unlock(); } } }
至此,上面的所有的問題已經全部完美的得到了解決!
希望以上對你理解wait¬ify,lock&condition有所幫助,也歡迎大家觀看我的兩個影片課程:
來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/3486/viewspace-2810383/,如需轉載,請註明出處,否則將追究法律責任。
相關文章
- Java併發程式設計之synchronizedJava程式設計synchronized
- Java併發程式設計之Java CAS操作Java程式設計
- Java 併發程式設計之 Condition 介面Java程式設計
- java併發程式設計系列:java併發程式設計背景知識Java程式設計
- 譯文《Java併發程式設計之CAS》Java程式設計
- 譯文《Java併發程式設計之volatile》Java程式設計
- 併發程式設計之 Java 三把鎖程式設計Java
- Java併發程式設計之原子變數Java程式設計變數
- Java併發程式設計Java程式設計
- java 併發程式設計Java程式設計
- Java併發程式設計之Java記憶體模型Java程式設計記憶體模型
- Java併發程式設計之鎖機制之AQSJava程式設計AQS
- Java併發程式設計系列之Semaphore詳解Java程式設計
- Java併發程式設計之CyclicBarrier使用指南Java程式設計
- Java併發程式設計 - 第十一章 Java併發程式設計實踐Java程式設計
- Java併發程式設計之鎖機制之Condition介面Java程式設計
- Java併發程式設計之鎖機制之LockSupport工具Java程式設計
- Java併發程式設計之鎖機制之Lock介面Java程式設計
- Java 併發程式設計解析Java程式設計
- Java併發程式設計——ThreadLocalJava程式設計thread
- java-併發程式設計Java程式設計
- Java併發程式設計-CASJava程式設計
- Java併發程式設計:LockJava程式設計
- Java併發程式設計—ThreadLocalJava程式設計thread
- Java併發程式設計:synchronizedJava程式設計synchronized
- Java併發程式設計 -- ThreadLocalJava程式設計thread
- Java併發程式設計 -- ConditionJava程式設計
- 併發程式設計之:Atomic程式設計
- 併發程式設計之:ThreadLocal程式設計thread
- 併發程式設計之:Lock程式設計
- 併發程式設計之:ForkJoin程式設計
- 併發程式設計之volatile程式設計
- 併發程式設計之:CountDownLatch程式設計CountDownLatch
- 併發程式設計之:synchronized程式設計synchronized
- 併發程式設計之:JMM程式設計
- Java併發程式設計---java規範與模式下的併發程式設計1.1Java程式設計模式
- Java併發程式設計-鎖及併發容器Java程式設計
- Java併發程式設計序列之JUC底層AQSJava程式設計AQS