《Java併發編程的藝術》第五章——Java中的鎖

知識點:

  1. Lock接口。
  2. AQS隊列同步器。
  3. 重入鎖和讀寫鎖。
  4. LockSupport工具。
  5. Condition接口。

1.Lock接口。
在Lock接口出現之前,Java程序靠synchronized關鍵字實現鎖功能,而Java SE5之後,併發包中新增了Lock接口用來實現鎖功能,他提供了與synchronized關鍵字類似的同步功能。但在使用時需要顯示的獲取和釋放鎖。雖然他缺少了隱式獲取和釋放鎖的便捷性,但卻擁有了鎖獲取與釋放的可操作性、可中斷性及超時獲取鎖等synchronized關鍵字不具備的同步特性。
Lock接口提供的synchronized關鍵字不具備的主要特性:
這裏寫圖片描述
Lock定義的獲取鎖和釋放鎖的基本操作:
這裏寫圖片描述
Lock是一個接口,其常用的實現ReentrantLock會在下文進行講解。
【備註】:如果在獲取鎖時發生了異常,異常拋出的同時也會導致鎖無故釋放。


2.AQS隊列同步器
隊列同步器AbstractQueuedSynchronizer(AQS),是用來構建鎖和其他同步組件的基礎框架,他使用一個int成員變量表示同步狀態,通過內置的FIFO隊列來完成資源獲取線程的排隊工作。
同步器的主要使用方式是繼承,子類通過繼承同步器並實現它的抽象方法來管理同步狀態。子類推薦被定義爲自定義同步組件的靜態內部類,同步器自身沒有實現任何同步接口,他僅僅是定義了若干同步狀態獲取和釋放的方法來供自定義同步組件來使用,同步器既可以支持獨佔式的獲取同步狀態,也可以支持共享式的獲取同步狀態,這樣就可以方便實現不同類型的同步組件:RenntrantLock、RenntrantReadWriterLock、CountDownLatch等。
【備註】:同步器和鎖的關係:鎖的面向使用者,它定義了使用者與鎖交互的接口,隱藏了實現細節;同步器面向的是鎖的實現者,它簡化了鎖的實現方式,屏蔽了同步狀態管理、線程的排隊、等待與喚醒等底層操作。
同步器的設計師是基於模板方法模式的,使用者需要繼承同步器並重寫制定的方法,並將同步器組合在自定義同步組件的實現中,當調用同步器的模板方法時,這些模板方法會調用使用者重寫的方法。
同步器可重寫的方法:
這裏寫圖片描述
同步器提供的模板方法:
這裏寫圖片描述
同步器提供的模板方法基本分爲3類:獨佔式獲取與釋放同步狀態、共享式獲取與釋放同步狀態和查詢同步隊列中的等待線程情況。


實例一:

package com.lipeng.fifth;

import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.AbstractQueuedSynchronizer;

/**
 * 自定義獨佔式同步組件Demo
 * @author LiPeng
 *
 */
public class MutexDemo {
    public static class Sync extends AbstractQueuedSynchronizer{
        //嘗試獲取獨佔同步狀態
        @Override
        protected boolean tryAcquire(int arg) {
            if(compareAndSetState(0, 1)){
                //設置獨佔同步狀態所有者爲當前線程
                setExclusiveOwnerThread(Thread.currentThread());
                return true;
            }
            return false;
        }
        //嘗試釋放獨佔同步狀態
        @Override
        protected boolean tryRelease(int arg) {
            if(getState()==0)
                throw new IllegalMonitorStateException();
            setState(0);
            setExclusiveOwnerThread(null);
            return true;
        }
        //是否被當前線程獨佔
        @Override
        protected boolean isHeldExclusively() {
            return getState()==1;
        }
    } 

    private final Sync sync=new Sync();

    /**
     * 獲取同步狀態
     */
    public void Lock(){
        sync.acquire(1);
    }
    /**
     * 釋放同步狀態
     * @return
     */
    public boolean unLock(){
        return sync.release(0);
    }
    /**
     * 嘗試獲取同步狀態
     * @return
     */
    public boolean tryLock(){
        return sync.tryAcquire(1);
    }
    /**
     * 嘗試釋放同步狀態
     * @return
     */
    public boolean tryUnLock(){
        return sync.tryRelease(1);
    }
    /**
     * 檢測當前線程是否獨佔同步狀態
     * @return
     */
    public boolean isHeldExclusively(){
        return sync.isHeldExclusively();
    }
    /**
     * 帶超時功能的獲取同步狀態
     * @param time
     * @param unit
     * @return
     * @throws InterruptedException
     */
    public boolean lockWithTimeout(long time,TimeUnit unit) throws InterruptedException{
        return sync.tryAcquireNanos(1, unit.toNanos(time));
    }
    /**
     * 響應中斷的獲取同步狀態
     * @throws InterruptedException
     */
    public void lockWithInterrupted() throws InterruptedException{
         sync.acquireInterruptibly(1);
    }


}

隊列同步器的實現分析
同步器依賴內部的同步隊列(一個FIFO雙向隊列)來完成同步狀態的管理,當前線程獲取同步狀態失敗時,同步器會將當前線程及等待狀態等信息構造成一個節點(Node)並將其加入同步隊列,同時會阻塞當前線程,當同步狀態釋放時,會把首節點中的線程喚醒,使其再次嘗試獲取同步狀態。
同步隊列中的節點(Node)用來保存獲取同步狀態失敗的線程引用、等待狀態以及前驅和後繼節點,節點的屬性類型與名稱如下圖:
這裏寫圖片描述
同步隊列的基本結構如下:
這裏寫圖片描述
當一個線程成功獲取同步狀態後,其他線程無法獲取到同步狀態時,獲取同步狀態失敗的線程被構造成節點,並以線程安全的方式加入同步隊列。
【備註】:compareAndSetTail(Node expect,Node update)方法是基於CAS的設置尾節點的方法,它需要傳遞當前線程“認爲”的尾節點和當前節點,只有設置成功後,當前節點才正式與之前的尾節點建立關聯。
設置尾節點的流程如下:
這裏寫圖片描述
同步隊列遵循FIFO,首節點是獲取同步狀態成功的節點,首節點的線程在釋放同步狀態時,會喚醒後繼節點,後繼節點在成功獲取同步狀態後,將自己設置爲首節點。
設置首節點的流程如下:
這裏寫圖片描述
因爲同一時間只有一個線程可以獲取到同步狀態,所以設置首節點的方法並不需要CAS來保證,只需要將原首節點的後繼節點設置爲新首節點即可。


獨佔式同步狀態獲取與釋放
這裏寫圖片描述
在獨佔式同步組件中,同一時刻只能有一個線程成功獲取同步狀態,如果其他線程獲取同步狀態失敗,則構造同步節點並通過addWaiter(Node node)方法將該節點加入同步隊列的尾部。然後調用acquireQueued(Node node,int arg)方法,以“死循環”的方式獲取同步狀態,如果獲取不到則阻塞節點中的線程,直到被其前驅節點喚醒或被中斷。
這裏寫圖片描述
當前驅節點爲首節點併成功獲取同步狀態後,設置自身爲新的首節點,並跳出死循環。
獨佔式同步組件獲取同步狀態流程如下:
這裏寫圖片描述
由上圖可知,只有當前驅節點爲首節點時,纔會嘗試獲取同步狀態,這是因爲:
- 只有前驅節點爲首節點,其在釋放同步狀態後,纔會喚醒後繼節點,後繼節點被喚醒後需要檢查自己的前驅節點是否爲首節點。
- 維護同步隊列FIFO原則並處理過早的通知。
【備註】:過早通知是指前驅節點不是首節點的線程由於中斷而被喚醒。
當前線程獲取同步狀態並執行了相應邏輯之後,就需要釋放同步狀態,使得後續節點能夠繼續獲取同步狀態。通過同步器的release(int arg)方法可以釋放同步狀態,該方法在釋放了同步狀態後,會喚醒後繼節點(unparkSuccessor(Node node))。
這裏寫圖片描述


獨佔式超時獲取同步狀態
通過調用同步器的doAcquireNanos(int arg,long nanosTimeout)方法可以超時獲取同步狀態,即在指定的時間段內獲取同步狀態,如果獲取到同步狀態則返回true,否則,返回false。該方法提供了傳統Java同步操作(如:Synchronized關鍵字)所不具備的特性。
Synchronized關鍵字不會響應中斷,即如果一個線程阻塞在Synchronized上,等待獲取鎖,如果對該線程進行中斷操作,其中斷標誌位會被修改,但線程仍然會阻塞。
在Java5中,同步器提供了響應中斷的獲取同步狀態的方法:acquireInterruptibly(int arg),此方法在等待獲取同步狀態時,如果當前線程被中斷,會立刻返回,並拋出InterruptedException。
而超時獲取同步狀態的實現原理就是超時時間加上響應中斷的獲取同步狀態。
這裏寫圖片描述
該方法在自旋過程中,當節點的前驅節點爲首節點時嘗試獲取同步狀態,如果獲取成功則從方法返回並設置自身爲新的首節點。如果獲取失敗,則判斷是否超時,如果沒有超時,重新計算超時間隔nanosTimeout,如果nanosTimeout小於等於spinForTimeoutThreshold(1000納秒)時,則進入快速自旋過程,否則使當前線程等待nanosTimeout納秒。
【備註】:之所以在小於等於1000納秒就會“提前”進入快速自旋過程的原因在於,非常短的超時等待無法做到十分精確,如果這是再進行超時等待,會讓nanosTimeout的超時從整體上表現得不精確。
流程圖如下:
這裏寫圖片描述


共享式同步狀態獲取與釋放
共享式同步組件與獨佔式同步組件最主要的區別是,共享式同步組件允許在同一時刻有多個線程同時獲取到同步狀態。以文件的讀寫爲例,如果一個程序在對文件進行讀操作,那麼同一時刻對於該文件的寫操作均被阻塞,而讀操作能夠同時進行。寫操作要求對資源的獨佔式訪問,而讀操作可以是共享式訪問。
這裏寫圖片描述
通過調用同步器的acquireShared(int arg)方法可以共享式的獲取同步狀態。在acquireShared(int arg)方法中,同步器調用tryAcquireShared(int arg)方法嘗試獲取同步狀態,tryAcquireShared(int arg)方法返回值爲int類型,當返回值大於等於0時,表示能夠獲取到同步狀態。因此,在共享式獲取的自旋過程中,如果當前節點的前驅爲首節點,並嘗試獲取同步狀態時返回值大於等於0,則表示獲取同步狀態成功並退出自旋過程。
共享式同步組件通過調用releaseShared(int arg)方法釋放同步狀態:
這裏寫圖片描述
該方法在釋放同步狀態之後,將會喚醒後續處於等待狀態的節點。它與獨佔式同步組件區別在於,必須確保同步狀態線程安全的釋放,因爲釋放同步狀態的操作可能同時來自多個線程,一般是通過循環和CAS來保證。


實例一:

package com.lipeng.fifth;

import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.AbstractQueuedSynchronizer;
/**
 * 自定義同步組件
 * 同一時刻只允許兩個線程獲取同步狀態
 * @author LiPeng
 *
 */
public class SharedLock {
    private final Sync sync=new Sync(2);
    private static class Sync extends AbstractQueuedSynchronizer{
        Sync(int count){
            if(count<=0)
                throw new IllegalArgumentException("count must large than 0");
            setState(count);
        }

        @Override
        protected int tryAcquireShared(int reduceCount) {
            for(;;){
                int current=getState();
                int newCount=current-reduceCount;
                //允許嘗試獲取同步狀態數量大等0,且成功獲取同步狀態後,跳出自旋
                if(newCount>=0&&compareAndSetState(current, newCount)){
                    return newCount;
                }
            }
        }
        @Override
        protected boolean tryReleaseShared(int returnCount) {
            for(;;){
                int current=getState();
                int newCount=current+returnCount;
                if(compareAndSetState(current, newCount)){
                    return true;
                }
            }
        }
    }
    public void lock(){
        sync.tryAcquireShared(1);
    }
    public void unLock(){
        sync.tryReleaseShared(1);
    }


    //測試
    public static void main(String[] args) {
        final SharedLock sharedLock=new SharedLock();
        for(int i=0;i<10;++i){
            new Thread(new Runnable() {
                @Override
                public void run() {
                    sharedLock.lock();
                    try {
                        //睡眠3秒
                        System.out.println(Thread.currentThread().getName()+" get lock");
                        TimeUnit.SECONDS.sleep(3);
                    } catch (InterruptedException e) {
                        // TODO Auto-generated catch block
                        e.printStackTrace();
                    }
                    sharedLock.unLock();
                }
            },"Thread-"+i).start();;
        }
    }

}

3.重入鎖和讀寫鎖
3.1 重入鎖
重入鎖顧名思義就是支持重進入的鎖,它表示該鎖能夠支持一個線程對資源的重複加鎖,除此之外,該鎖還支持獲取鎖時的公平和非公平性選擇,即如果在絕對時間上,先對鎖進行獲取的請求一定先被滿足,那麼這個鎖是公平的,反之,是不公平的。ReentrantLock提供一個構造函數來控制鎖是否是公平的。
實現重入鎖,必須解決以下問題:
- 線程再次獲取鎖:鎖需要識別獲取鎖的進程是否是當前佔用鎖的進程,如果是,則獲取成功。
- 鎖的釋放:線程重複獲取了N次鎖,隨後在第N次釋放鎖後,其他線程才能夠獲取到該鎖。鎖在被獲取時進行計數自增,在釋放時,計數自減,當計數等於0時表示鎖成功釋放。
非公平的重入鎖獲取同步狀態:
這裏寫圖片描述
如果無線程佔用鎖,則使用CAS算法設置新的狀態,若成功,則成功獲取鎖。否則,判斷當前線程是否是佔用鎖的線程,如果是,則設置新的狀態併成功獲取鎖。
公平的重入鎖獲取同步狀態:
這裏寫圖片描述
與非公平的重入鎖相比,公平鎖唯一的改變首次獲取同步狀態時,會通過調用hasQueuedPredecessors()方法來判斷當前節點是否包含前驅節點,以此保證公平的獲取鎖。
重入鎖釋放同步狀態:
這裏寫圖片描述
【備註】:公平鎖雖然保證了鎖的獲取按照FIFO原則,但代價就是會進行大量的線程切換,導致性能下降。而非公平鎖雖然可能造成線程“飢餓”,但極少的線程切換,保證了其更大的吞吐量。
3.2 讀寫鎖
上面介紹的鎖基本都是排它鎖,這些鎖在同一時刻只允許一個線程進行訪問,而讀寫鎖在同一時刻可以允許多個讀線程訪問,但在寫進程訪問時,所有的讀進程和其他寫進程均被阻塞。讀寫鎖通過分離讀鎖和寫鎖,使併發性相比一般的排他鎖有了很大提升。在讀多於寫的情況下,讀寫鎖能夠提供比排它鎖更好的併發性和吞吐量。Java併發包提供讀寫鎖的實現是ReentrantReadWriteLock。它提供的特性如下:
這裏寫圖片描述
ReadWriteLock接口僅定義了獲取讀鎖和寫鎖的兩個方法,即readLock()和writeLock()方法,而其實現:ReentrantReadWriteLock除此之外還提供了便於外界監控其工作狀態的方法:
這裏寫圖片描述


實例一:

package com.lipeng.fifth;

import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock.ReadLock;
import java.util.concurrent.locks.ReentrantReadWriteLock.WriteLock;
/**
 * 使用讀寫鎖的緩存Demo
 * @author LiPeng
 *
 */
public class CacheDemo {
    private static ReentrantReadWriteLock lock=new ReentrantReadWriteLock();
    private static WriteLock writeLock=lock.writeLock();
    private static ReadLock readLock=lock.readLock();
    private static Map<String,String> cache=new HashMap<String,String>();

    //讀鎖
    public static String getValByKey(String key){
        try {
            System.out.println(Thread.currentThread().getName()+" try get read lock ,TimeStrap:"+System.currentTimeMillis());
            readLock.lock();
            System.out.println(Thread.currentThread().getName()+" got read lock ,TimeStrap:"+System.currentTimeMillis());
            TimeUnit.SECONDS.sleep(10);
            return cache.get(key);
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }finally{
            readLock.unlock();
        }
    }

    //寫鎖
    public static String setVal(String key,String val){
        try {
            System.out.println(Thread.currentThread().getName()+" try get write lock ,TimeStrap:"+System.currentTimeMillis());
            writeLock.lock();
            System.out.println(Thread.currentThread().getName()+" got write lock ,TimeStrap:"+System.currentTimeMillis());
            return cache.put(key, val);
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }finally{
            writeLock.unlock();
        }
    }

    public static void main(String[] args) {
        Thread thread1=new Thread(new Runnable() {
            @Override
            public void run() {
                CacheDemo.getValByKey("1");
            }
        },"Thread-1");
        Thread thread2=new Thread(new Runnable() {
            @Override
            public void run() {
                CacheDemo.getValByKey("2");
            }
        },"Thread-2");
        Thread thread3=new Thread(new Runnable() {
            @Override
            public void run() {
                CacheDemo.setVal("1","1");
            }
        },"Thread-3");
        try {
            thread1.start();
            TimeUnit.SECONDS.sleep(2);
            thread2.start();
            TimeUnit.SECONDS.sleep(2);
            thread3.start();
        } catch (Exception e) {
            e.printStackTrace();
        }

    }
}

運行截圖:
這裏寫圖片描述


讀寫鎖實現的分析
讀寫鎖同樣依賴AQS實現,其讀寫狀態就是同步器的同步狀態。因爲同步器中表示同步狀態的是一個整形變量,於是需要“按位切割使用”,高16位表示讀狀態,低16位表示寫狀態。
如下圖:
這裏寫圖片描述
寫鎖的獲取與釋放
寫鎖是一個支持重進入的排它鎖。如果當前線程已經獲取寫鎖,則增加狀態。如果當前線程在獲取寫鎖時,讀鎖已經被獲取(讀狀態不爲0)或者該線程不是當前佔用寫鎖的線程,則進入等待狀態。如果允許在讀寫被獲取的情況下,仍然可獲取寫鎖,那麼正在運行的線程將無法感知獲取寫鎖線程的操作。
這裏寫圖片描述
寫鎖的釋放與重入鎖的釋放過程基本類似,每次釋放均減少寫狀態,當寫狀態爲0時表示寫鎖已被釋放。
讀鎖的獲取和釋放
讀鎖是一個支持重入的共享鎖,它能被多個線程同時獲取,在沒有其他寫線程訪問時,讀鎖總會被成功獲取,併線程安全的增加讀狀態。如果有線程獲取了寫鎖,則會進入等待狀態。
這裏寫圖片描述
在tryAcquireShared(int unused)方法中,如果其他線程已經獲取了寫鎖,則當前線程獲取讀鎖失敗,進入等待狀態。如果當前線程獲取了寫鎖或者寫鎖未被獲取,則當前線程增加讀狀態,成功獲取讀鎖。
【備註】:同一線程,在已經獲取寫鎖的情況下,仍然可獲取讀寫,因爲其“寫”動作可以被“讀”感知到。
鎖降級
鎖降級是指寫鎖降級成爲讀鎖。線程在獲取寫鎖後,再獲取讀鎖,隨後釋放寫鎖,則寫鎖降級爲讀鎖。
之所以是先獲取讀鎖,再釋放寫鎖的原因是,如果不獲取讀鎖,直接釋放寫鎖,那麼如果此時有其他線程獲取寫鎖並修改數據後,當前線程無法感知數據的更新。如果當前線程先獲取讀鎖再釋放寫所得話,其他線程在當前線程佔有讀鎖時,無法獲取到寫鎖,直到當前線程釋放。
RentrantReadWriteLock不支持鎖升級(在佔用讀鎖時,獲取寫鎖,然後釋放讀鎖),目的同樣是爲了保證數據的可見性。如果讀鎖已經被多個線程獲取,其中任意線程獲取寫鎖並更新數據後,更新後的數據對其他獲取到讀鎖的線程不可見。
4. LockSupport工具
當需要阻塞或喚醒一個線程的時候,都會使用LockSupport工具類來完成相應工作。LockSupport定義了一組公共靜態方法,提供最基本的線程阻塞和喚醒功能。LockSupport也是構建同步組件的基礎工具。
這裏寫圖片描述
Java6中,LockSupport增加了park(Object blocker)、parkNanos(Object blocker,long nanos)和parkUtil(Object blocker,long deadline)3個方法,用於實現阻塞當前線程的功能,其中參數blocker用來標識當前線程在等待的對象,爲線程dump提供監控使用。
5. Condition接口
任意一個Java對象,都擁有一組監視器方法(wait及notify等),與synchronized關鍵字配合,可以實現等待/通知模式。Condition接口也提供了類似Object的監視器方法,與Lock配合可以實現等待/通知模式。兩者在使用方式及功能特性上差別如下:
這裏寫圖片描述
Condition定義了等待/通知兩種類型的方法,當前線程調用這些方法時,需要提前獲取到Condition對象關聯的鎖,Condition對象是由Lock對象創建出來的,換句話說,Condition是依賴Lock對象的。


實例一:

package com.lipeng.fifth;

import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/**
 * Condition 實現等待/通知模式
 * @author LiPeng
 *
 */
public class WaitAndNoifyDemo {

    static Lock lock=new ReentrantLock();
    static Condition condition=lock.newCondition();

    public static void main(String[] args) {
        try {
            new Thread(new WaitAction(lock,condition),"WaitAndNoifyDemo-WaitThread").start();
            //sleep 3s
            TimeUnit.SECONDS.sleep(3);
            new Thread(new NotifyAction(lock,condition),"WaitAndNoifyDemo-NotifyThread").start();   
        } catch (InterruptedException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }

    }
}
class WaitAction implements Runnable{
    Lock lock;
    Condition condition;
    public WaitAction(Lock lock, Condition condition) {
        super();
        this.lock = lock;
        this.condition = condition;
    }

    @Override
    public void run() {
        try {
            System.out.println(Thread.currentThread().getName()+" try lock...");
            lock.lock();
            System.out.println(Thread.currentThread().getName()+" get lock and await...");
            condition.await();
            System.err.println(Thread.currentThread().getName()+" notify and start do something...");
            lock.unlock();
            System.out.println(Thread.currentThread().getName()+" unlock...");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

}
class NotifyAction implements Runnable{
    Lock lock;
    Condition condition;
    public NotifyAction(Lock lock, Condition condition) {
        super();
        this.lock = lock;
        this.condition = condition;
    }
    @Override
    public void run() {
        try {
            System.out.println(Thread.currentThread().getName()+" try lock...");
            lock.lock();
            System.out.println(Thread.currentThread().getName()+" get lock and signal other thread...");
            condition.signal();
            lock.unlock();
            System.out.println(Thread.currentThread().getName()+" unlock...");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

}



Condition的實現分析
ConditionObject是同步器AQS的內部類,因爲Condition的操作需要獲取相關的所。每個Condition對象都包含一個隊列(即等待隊列),該隊列是Condition對象實現等待/通知功能的關鍵。
等待隊列
等待隊列是一個FIFO的隊列,在隊列中的每個節點都包含一個線程引用,該線程就是在Condition對象上的等待的線程,如果一個線程調用了Condition.await()方法,則該線程會釋放鎖、構造節點加入等待隊列並進入等待狀態。此節與同步器中節點定義一致,均是AbstractQueuedSynchronizer.Node。
一個Condition包含一個等待隊列,並擁有首節點和尾節點的引用,當線程調用了Condition.await()方法,將會構造節點,加入隊列尾部,並將原尾結點nextWaiter指向它,並更新尾節點。此過程不需要CAS保證,因爲執行此操作的線程,必定是獲取了鎖的線程。
其基本結構如下:
這裏寫圖片描述
在Object的監視器模型上,一個對象擁有一個同步隊列和等待隊列,而併發包中的Lock(確切的說是同步器)擁有一個同步隊列和多個等待隊列 ,其對應關係如下:
這裏寫圖片描述
等待
調用Condition的await()方法,會使當前線程進入等待隊列並釋放鎖,然後喚醒同步隊列中的後繼節點,最後當前線程變爲等待狀態。
如果從隊列角度看await()方法,當調用await()方法時,相當於同步隊列的首節點移動到了Condition的等待隊列中。
這裏寫圖片描述
當前線程加入等待隊列示意圖:
這裏寫圖片描述
通知
調用Condition的signal()方法,會將等待隊列中的首節點移動到同步隊列中,並將其喚醒,被喚醒的線程嘗試獲取同步狀態。(如果不是同步調用signal()方法喚醒,而是對等待線程進行終端,則會拋出InterruptException),如果成功獲取鎖,則被喚醒的線程從await()方法返回。
這裏寫圖片描述
節點從等待隊列移動到同步隊列示意圖:
這裏寫圖片描述
Condition的signalAll()方法,相當於等待隊列中的每個節點均被執行一次signal()方法,效果就是將等待隊列中所有節點全部移動到同步隊列中,並喚醒每個節點的線程。

【備註】:本文圖片均摘自《Java併發編程的藝術》·方騰飛,若本文有錯或不恰當的描述,請各位不吝斧正。謝謝!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章