java併發程式設計之wait&notify VS lock&am

lvxfcjf發表於2021-09-09

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 GrumpBoundedBufer extends 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 SleepyBoundedBufer extends 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 BoundedBufer extends 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 ConditionalBoundedBufer extends 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&notify,lock&condition有所幫助,也歡迎大家觀看我的兩個影片課程:

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/3486/viewspace-2810383/,如需轉載,請註明出處,否則將追究法律責任。

相關文章