深入剖析基於併發AQS的(獨佔鎖)重入鎖(ReetrantLock)及其Condition實現原理

【版權申明】未經博主同意,謝絕轉載!(請尊重原創,博主保留追究權)
http://blog.csdn.net/javazejian/article/details/75043422
出自【zejian的博客】

關聯文章:

深入理解Java類型信息(Class對象)與反射機制

深入理解Java枚舉類型(enum)

深入理解Java註解類型(@Annotation)

深入理解Java類加載器(ClassLoader)

深入理解Java併發之synchronized實現原理

Java併發編程-無鎖CAS與Unsafe類及其併發包Atomic

深入理解Java內存模型(JMM)及volatile關鍵字

剖析基於併發AQS的重入鎖(ReetrantLock)及其Condition實現原理

剖析基於併發AQS的共享鎖的實現(基於信號量Semaphore)

併發之阻塞隊列LinkedBlockingQueue與ArrayBlockingQueue

在閱讀本篇博文前,建議有CAS知識儲備,因爲關於CAS的操作在ReetrantLock的實現原理中可是隨處可見,如沒有了解過CAS可以先看博主的另一篇博文【Java併發編程-無鎖CAS與Unsafe類及其併發包Atomic】,以下是本篇的主要內容

Lock接口

前面我們詳談過解決多線程同步問題的關鍵字synchronized,synchronized屬於隱式鎖,即鎖的持有與釋放都是隱式的,我們無需干預,而本篇我們要講解的是顯式鎖,即鎖的持有和釋放都必須由我們手動編寫。在Java 1.5中,官方在concurrent併發包中加入了Lock接口,該接口中提供了lock()方法和unLock()方法對顯式加鎖和顯式釋放鎖操作進行支持,簡單瞭解一下代碼編寫,如下:

Lock lock = new ReentrantLock();
lock.lock();
try{
    //臨界區......
}finally{
    lock.unlock();
}

正如代碼所顯示(ReentrantLock是Lock的實現類,稍後分析),當前線程使用lock()方法與unlock()對臨界區進行包圍,其他線程由於無法持有鎖將無法進入臨界區直到當前線程釋放鎖,注意unlock()操作必須在finally代碼塊中,這樣可以確保即使臨界區執行拋出異常,線程最終也能正常釋放鎖,Lock接口還提供了鎖以下相關方法

public interface Lock {
    //加鎖
    void lock();

    //解鎖
    void unlock();

    //可中斷獲取鎖,與lock()不同之處在於可響應中斷操作,即在獲
    //取鎖的過程中可中斷,注意synchronized在獲取鎖時是不可中斷的
    void lockInterruptibly() throws InterruptedException;

    //嘗試非阻塞獲取鎖,調用該方法後立即返回結果,如果能夠獲取則返回true,否則返回false
    boolean tryLock();

    //根據傳入的時間段獲取鎖,在指定時間內沒有獲取鎖則返回false,如果在指定時間內當前線程未被中並斷獲取到鎖則返回true
    boolean tryLock(long time, TimeUnit unit) throws InterruptedException;

    //獲取等待通知組件,該組件與當前鎖綁定,當前線程只有獲得了鎖
    //才能調用該組件的wait()方法,而調用後,當前線程將釋放鎖。
    Condition newCondition();

可見Lock對象鎖還提供了synchronized所不具備的其他同步特性,如可中斷鎖的獲取(synchronized在等待獲取鎖時是不可中的),超時中斷鎖的獲取,等待喚醒機制的多條件變量Condition等,這也使得Lock鎖在使用上具有更大的靈活性。下面進一步分析Lock的實現類重入鎖ReetrantLock。

重入鎖ReetrantLock

重入鎖ReetrantLock,JDK 1.5新增的類,實現了Lock接口,作用與synchronized關鍵字相當,但比synchronized更加靈活。ReetrantLock本身也是一種支持重進入的鎖,即該鎖可以支持一個線程對資源重複加鎖,同時也支持公平鎖與非公平鎖。所謂的公平與非公平指的是在請求先後順序上,先對鎖進行請求的就一定先獲取到鎖,那麼這就是公平鎖,反之,如果對於鎖的獲取並沒有時間上的先後順序,如後請求的線程可能先獲取到鎖,這就是非公平鎖,一般而言非,非公平鎖機制的效率往往會勝過公平鎖的機制,但在某些場景下,可能更注重時間先後順序,那麼公平鎖自然是很好的選擇。需要注意的是ReetrantLock支持對同一線程重加鎖,但是加鎖多少次,就必須解鎖多少次,這樣纔可以成功釋放鎖。下面看看ReetrantLock的簡單使用案例:

import java.util.concurrent.locks.ReentrantLock;

public class ReenterLock implements Runnable{
    public static ReentrantLock lock=new ReentrantLock();
    public static int i=0;
    @Override
    public void run() {
        for(int j=0;j<10000000;j++){
            lock.lock();
            //支持重入鎖
            lock.lock();
            try{
                i++;
            }finally{
                //執行兩次解鎖
                lock.unlock();
                lock.unlock();
            }
        }
    }
    public static void main(String[] args) throws InterruptedException {
        ReenterLock tl=new ReenterLock();
        Thread t1=new Thread(tl);
        Thread t2=new Thread(tl);
        t1.start();t2.start();
        t1.join();t2.join();
        //輸出結果:20000000
        System.out.println(i);
    }
}

代碼非常簡單,我們使用兩個線程同時操作臨界資源i,執行自增操作,使用ReenterLock進行加鎖,解決線程安全問題,這裏進行了兩次重複加鎖,由於ReenterLock支持重入,因此這樣是沒有問題的,需要注意的是在finally代碼塊中,需執行兩次解鎖操作才能真正成功地讓當前執行線程釋放鎖,從這裏看ReenterLock的用法還是非常簡單的,除了實現Lock接口的方法,ReenterLock其他方法說明如下

//查詢當前線程保持此鎖的次數。
int getHoldCount() 

//返回目前擁有此鎖的線程,如果此鎖不被任何線程擁有,則返回 null。      
protected  Thread   getOwner(); 

//返回一個 collection,它包含可能正等待獲取此鎖的線程,其內部維持一個隊列,這點稍後會分析。      
protected  Collection<Thread>   getQueuedThreads(); 

//返回正等待獲取此鎖的線程估計數。   
int getQueueLength();

// 返回一個 collection,它包含可能正在等待與此鎖相關給定條件的那些線程。
protected  Collection<Thread>   getWaitingThreads(Condition condition); 

//返回等待與此鎖相關的給定條件的線程估計數。       
int getWaitQueueLength(Condition condition);

// 查詢給定線程是否正在等待獲取此鎖。     
boolean hasQueuedThread(Thread thread); 

//查詢是否有些線程正在等待獲取此鎖。     
boolean hasQueuedThreads();

//查詢是否有些線程正在等待與此鎖有關的給定條件。     
boolean hasWaiters(Condition condition); 

//如果此鎖的公平設置爲 true,則返回 true。     
boolean isFair() 

//查詢當前線程是否保持此鎖。      
boolean isHeldByCurrentThread() 

//查詢此鎖是否由任意線程保持。        
boolean isLocked()       

由於ReetrantLock鎖在使用上還是比較簡單的,也就暫且打住,下面着重分析一下ReetrantLock的內部實現原理,這纔是本篇博文的重點。實際上ReetrantLock是基於AQS併發框架實現的,我們先深入瞭解AQS,然後一步步揭開ReetrantLock的內部實現原理。

併發基礎組件AQS與ReetrantLock

AQS工作原理概要

AbstractQueuedSynchronizer又稱爲隊列同步器(後面簡稱AQS),它是用來構建鎖或其他同步組件的基礎框架,內部通過一個int類型的成員變量state來控制同步狀態,當state=0時,則說明沒有任何線程佔有共享資源的鎖,當state=1時,則說明有線程目前正在使用共享變量,其他線程必須加入同步隊列進行等待,AQS內部通過內部類Node構成FIFO的同步隊列來完成線程獲取鎖的排隊工作,同時利用內部類ConditionObject構建等待隊列,當Condition調用wait()方法後,線程將會加入等待隊列中,而當Condition調用signal()方法後,線程將從等待隊列轉移動同步隊列中進行鎖競爭。注意這裏涉及到兩種隊列,一種的同步隊列,當線程請求鎖而等待的後將加入同步隊列等待,而另一種則是等待隊列(可有多個),通過Condition調用await()方法釋放鎖後,將加入等待隊列。關於Condition的等待隊列我們後面再分析,這裏我們先來看看AQS中的同步隊列模型,如下

/**
 * AQS抽象類
 */
public abstract class AbstractQueuedSynchronizer
    extends AbstractOwnableSynchronizer{
//指向同步隊列隊頭
private transient volatile Node head;

//指向同步的隊尾
private transient volatile Node tail;

//同步狀態,0代表鎖未被佔用,1代表鎖已被佔用
private volatile int state;

//省略其他代碼......
}

head和tail分別是AQS中的變量,其中head指向同步隊列的頭部,注意head爲空結點,不存儲信息。而tail則是同步隊列的隊尾,同步隊列採用的是雙向鏈表的結構這樣可方便隊列進行結點增刪操作。state變量則是代表同步狀態,執行當線程調用lock方法進行加鎖後,如果此時state的值爲0,則說明當前線程可以獲取到鎖(在本篇文章中,鎖和同步狀態代表同一個意思),同時將state設置爲1,表示獲取成功。如果state已爲1,也就是當前鎖已被其他線程持有,那麼當前執行線程將被封裝爲Node結點加入同步隊列等待。其中Node結點是對每一個訪問同步代碼的線程的封裝,從圖中的Node的數據結構也可看出,其包含了需要同步的線程本身以及線程的狀態,如是否被阻塞,是否等待喚醒,是否已經被取消等。每個Node結點內部關聯其前繼結點prev和後繼結點next,這樣可以方便線程釋放鎖後快速喚醒下一個在等待的線程,Node是AQS的內部類,其數據結構如下:

static final class Node {
    //共享模式
    static final Node SHARED = new Node();
    //獨佔模式
    static final Node EXCLUSIVE = null;

    //標識線程已處於結束狀態
    static final int CANCELLED =  1;
    //等待被喚醒狀態
    static final int SIGNAL    = -1;
    //條件狀態,
    static final int CONDITION = -2;
    //在共享模式中使用表示獲得的同步狀態會被傳播
    static final int PROPAGATE = -3;

    //等待狀態,存在CANCELLED、SIGNAL、CONDITION、PROPAGATE 4種
    volatile int waitStatus;

    //同步隊列中前驅結點
    volatile Node prev;

    //同步隊列中後繼結點
    volatile Node next;

    //請求鎖的線程
    volatile Thread thread;

    //等待隊列中的後繼結點,這個與Condition有關,稍後會分析
    Node nextWaiter;

    //判斷是否爲共享模式
    final boolean isShared() {
        return nextWaiter == SHARED;
    }

    //獲取前驅結點
    final Node predecessor() throws NullPointerException {
        Node p = prev;
        if (p == null)
            throw new NullPointerException();
        else
            return p;
    }

    //.....
}

其中SHARED和EXCLUSIVE常量分別代表共享模式和獨佔模式,所謂共享模式是一個鎖允許多條線程同時操作,如信號量Semaphore採用的就是基於AQS的共享模式實現的,而獨佔模式則是同一個時間段只能有一個線程對共享資源進行操作,多餘的請求線程需要排隊等待,如ReentranLock。變量waitStatus則表示當前被封裝成Node結點的等待狀態,共有4種取值CANCELLED、SIGNAL、CONDITION、PROPAGATE。

  • CANCELLED:值爲1,在同步隊列中等待的線程等待超時或被中斷,需要從同步隊列中取消該Node的結點,其結點的waitStatus爲CANCELLED,即結束狀態,進入該狀態後的結點將不會再變化。

  • SIGNAL:值爲-1,被標識爲該等待喚醒狀態的後繼結點,當其前繼結點的線程釋放了同步鎖或被取消,將會通知該後繼結點的線程執行。說白了,就是處於喚醒狀態,只要前繼結點釋放鎖,就會通知標識爲SIGNAL狀態的後繼結點的線程執行。

  • CONDITION:值爲-2,與Condition相關,該標識的結點處於等待隊列中,結點的線程等待在Condition上,當其他線程調用了Condition的signal()方法後,CONDITION狀態的結點將從等待隊列轉移到同步隊列中,等待獲取同步鎖。

  • PROPAGATE:值爲-3,與共享模式相關,在共享模式中,該狀態標識結點的線程處於可運行狀態。

  • 0狀態:值爲0,代表初始化狀態。

pre和next,分別指向當前Node結點的前驅結點和後繼結點,thread變量存儲的請求鎖的線程。nextWaiter,與Condition相關,代表等待隊列中的後繼結點,關於這點這裏暫不深入,後續會有更詳細的分析,嗯,到此我們對Node結點的數據結構也就比較清晰了。總之呢,AQS作爲基礎組件,對於鎖的實現存在兩種不同的模式,即共享模式(如Semaphore)和獨佔模式(如ReetrantLock),無論是共享模式還是獨佔模式的實現類,其內部都是基於AQS實現的,也都維持着一個虛擬的同步隊列,當請求鎖的線程超過現有模式的限制時,會將線程包裝成Node結點並將線程當前必要的信息存儲到node結點中,然後加入同步隊列等會獲取鎖,而這系列操作都有AQS協助我們完成,這也是作爲基礎組件的原因,無論是Semaphore還是ReetrantLock,其內部絕大多數方法都是間接調用AQS完成的,下面是AQS整體類圖結構

這裏以ReentrantLock爲例,簡單講解ReentrantLock與AQS的關係

  • AbstractOwnableSynchronizer:抽象類,定義了存儲獨佔當前鎖的線程和獲取的方法

  • AbstractQueuedSynchronizer:抽象類,AQS框架核心類,其內部以虛擬隊列的方式管理線程的鎖獲取與鎖釋放,其中獲取鎖(tryAcquire方法)和釋放鎖(tryRelease方法)並沒有提供默認實現,需要子類重寫這兩個方法實現具體邏輯,目的是使開發人員可以自由定義獲取鎖以及釋放鎖的方式。

  • Node:AbstractQueuedSynchronizer 的內部類,用於構建虛擬隊列(鏈表雙向鏈表),管理需要獲取鎖的線程。

  • Sync:抽象類,是ReentrantLock的內部類,繼承自AbstractQueuedSynchronizer,實現了釋放鎖的操作(tryRelease()方法),並提供了lock抽象方法,由其子類實現。

  • NonfairSync:是ReentrantLock的內部類,繼承自Sync,非公平鎖的實現類。

  • FairSync:是ReentrantLock的內部類,繼承自Sync,公平鎖的實現類。

  • ReentrantLock:實現了Lock接口的,其內部類有Sync、NonfairSync、FairSync,在創建時可以根據fair參數決定創建NonfairSync(默認非公平鎖)還是FairSync。

ReentrantLock內部存在3個實現類,分別是SyncNonfairSyncFairSync,其中Sync繼承自AQS實現瞭解鎖tryRelease()方法,而NonfairSync(非公平鎖)FairSync(公平鎖)則繼承自Sync,實現了獲取鎖的tryAcquire()方法,ReentrantLock的所有方法調用都通過間接調用AQS和Sync類及其子類來完成的。從上述類圖可以看出AQS是一個抽象類,但請注意其源碼中並沒一個抽象的方法,這是因爲AQS只是作爲一個基礎組件,並不希望直接作爲直接操作類對外輸出,而更傾向於作爲基礎組件,爲真正的實現類提供基礎設施,如構建同步隊列,控制同步狀態等,事實上,從設計模式角度來看,AQS採用的模板模式的方式構建的,其內部除了提供併發操作核心方法以及同步隊列操作外,還提供了一些模板方法讓子類自己實現,如加鎖操作以及解鎖操作,爲什麼這麼做?這是因爲AQS作爲基礎組件,封裝的是核心併發操作,但是實現上分爲兩種模式,即共享模式與獨佔模式,而這兩種模式的加鎖與解鎖實現方式是不一樣的,但AQS只關注內部公共方法實現並不關心外部不同模式的實現,所以提供了模板方法給子類使用,也就是說實現獨佔鎖,如ReentrantLock需要自己實現tryAcquire()方法和tryRelease()方法,而實現共享模式的Semaphore,則需要實現tryAcquireShared()方法和tryReleaseShared()方法,這樣做的好處是顯而易見的,無論是共享模式還是獨佔模式,其基礎的實現都是同一套組件(AQS),只不過是加鎖解鎖的邏輯不同罷了,更重要的是如果我們需要自定義鎖的話,也變得非常簡單,只需要選擇不同的模式實現不同的加鎖和解鎖的模板方法即可,AQS提供給獨佔模式和共享模式的模板方法如下

//AQS中提供的主要模板方法,由子類實現。
public abstract class AbstractQueuedSynchronizer
    extends AbstractOwnableSynchronizer{

    //獨佔模式下獲取鎖的方法
    protected boolean tryAcquire(int arg) {
        throw new UnsupportedOperationException();
    }

    //獨佔模式下解鎖的方法
    protected boolean tryRelease(int arg) {
        throw new UnsupportedOperationException();
    }

    //共享模式下獲取鎖的方法
    protected int tryAcquireShared(int arg) {
        throw new UnsupportedOperationException();
    }

    //共享模式下解鎖的方法
    protected boolean tryReleaseShared(int arg) {
        throw new UnsupportedOperationException();
    }
    //判斷是否爲持有獨佔鎖
    protected boolean isHeldExclusively() {
        throw new UnsupportedOperationException();
    }

}

在瞭解AQS的原理概要後,下面我們就基於ReetrantLock進一步分析AQS的實現過程,這也是ReetrantLock的內部實現原理。

基於ReetrantLock分析AQS獨佔模式實現過程

ReetrantLock中非公平鎖

AQS同步器的實現依賴於內部的同步隊列(FIFO的雙向鏈表對列)完成對同步狀態(state)的管理,當前線程獲取鎖(同步狀態)失敗時,AQS會將該線程以及相關等待信息包裝成一個節點(Node)並將其加入同步隊列,同時會阻塞當前線程,當同步狀態釋放時,會將頭結點head中的線程喚醒,讓其嘗試獲取同步狀態。關於同步隊列和Node結點,前面我們已進行了較爲詳細的分析,這裏重點分析一下獲取同步狀態和釋放同步狀態以及如何加入隊列的具體操作,這裏從ReetrantLock入手分析AQS的具體實現,我們先以非公平鎖爲例進行分析。

//默認構造,創建非公平鎖NonfairSync
public ReentrantLock() {
    sync = new NonfairSync();
}
//根據傳入參數創建鎖類型
public ReentrantLock(boolean fair) {
    sync = fair ? new FairSync() : new NonfairSync();
}

//加鎖操作
public void lock() {
     sync.lock();
}

前面說過sync是個抽象類,存在兩個不同的實現子類,這裏從非公平鎖入手,看看其實現

/**
 * 非公平鎖實現
 */
static final class NonfairSync extends Sync {
    //加鎖
    final void lock() {
        //執行CAS操作,獲取同步狀態
        if (compareAndSetState(0, 1))
       //成功則將獨佔鎖線程設置爲當前線程  
          setExclusiveOwnerThread(Thread.currentThread());
        else
            //否則再次請求同步狀態
            acquire(1);
    }
}

這裏獲取鎖時,首先對同步狀態執行CAS操作,嘗試把state的狀態從0設置爲1,如果返回true則代表獲取同步狀態成功,也就是當前線程獲取鎖成,可操作臨界資源,如果返回false,則表示已有線程持有該同步狀態(其值爲1),獲取鎖失敗,注意這裏存在併發的情景,也就是可能同時存在多個線程設置state變量,因此是CAS操作保證了state變量操作的原子性。返回false後,執行 acquire(1)方法,該方法是AQS中的方法,它對中斷不敏感,即使線程獲取同步狀態失敗,進入同步隊列,後續對該線程執行中斷操作也不會從同步隊列中移出,方法如下

public final void acquire(int arg) {
    //再次嘗試獲取同步狀態
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}

這裏傳入參數arg表示要獲取同步狀態後設置的值(即要設置state的值),因爲要獲取鎖,而status爲0時是釋放鎖,1則是獲取鎖,所以這裏一般傳遞參數爲1,進入方法後首先會執行tryAcquire(arg)方法,在前面分析過該方法在AQS中並沒有具體實現,而是交由子類實現,因此該方法是由ReetrantLock類內部實現的

//NonfairSync類
static final class NonfairSync extends Sync {

    protected final boolean tryAcquire(int acquires) {
         return nonfairTryAcquire(acquires);
     }
 }

//Sync類
abstract static class Sync extends AbstractQueuedSynchronizer {

  //nonfairTryAcquire方法
  final boolean nonfairTryAcquire(int acquires) {
      final Thread current = Thread.currentThread();
      int c = getState();
      //判斷同步狀態是否爲0,並嘗試再次獲取同步狀態
      if (c == 0) {
          //執行CAS操作
          if (compareAndSetState(0, acquires)) {
              setExclusiveOwnerThread(current);
              return true;
          }
      }
      //如果當前線程已獲取鎖,屬於重入鎖,再次獲取鎖後將status值加1
      else if (current == getExclusiveOwnerThread()) {
          int nextc = c + acquires;
          if (nextc < 0) // overflow
              throw new Error("Maximum lock count exceeded");
          //設置當前同步狀態,當前只有一個線程持有鎖,因爲不會發生線程安全問題,可以直接執行 setState(nextc);
          setState(nextc);
          return true;
      }
      return false;
  }
  //省略其他代碼
}

從代碼執行流程可以看出,這裏做了兩件事,一是嘗試再次獲取同步狀態,如果獲取成功則將當前線程設置爲OwnerThread,否則失敗,二是判斷當前線程current是否爲OwnerThread,如果是則屬於重入鎖,state自增1,並獲取鎖成功,返回true,反之失敗,返回false,也就是tryAcquire(arg)執行失敗,返回false。需要注意的是nonfairTryAcquire(int acquires)內部使用的是CAS原子性操作設置state值,可以保證state的更改是線程安全的,因此只要任意一個線程調用nonfairTryAcquire(int acquires)方法並設置成功即可獲取鎖,不管該線程是新到來的還是已在同步隊列的線程,畢竟這是非公平鎖,並不保證同步隊列中的線程一定比新到來線程請求(可能是head結點剛釋放同步狀態然後新到來的線程恰好獲取到同步狀態)先獲取到鎖,這點跟後面還會講到的公平鎖不同。ok~,接着看之前的方法acquire(int arg)

public final void acquire(int arg) {
    //再次嘗試獲取同步狀態
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}

如果tryAcquire(arg)返回true,acquireQueued自然不會執行,這是最理想的,因爲畢竟當前線程已獲取到鎖,如果tryAcquire(arg)返回false,則會執行addWaiter(Node.EXCLUSIVE)進行入隊操作,由於ReentrantLock屬於獨佔鎖,因此結點類型爲Node.EXCLUSIVE,下面看看addWaiter方法具體實現

private Node addWaiter(Node mode) {
    //將請求同步狀態失敗的線程封裝成結點
    Node node = new Node(Thread.currentThread(), mode);

    Node pred = tail;
    //如果是第一個結點加入肯定爲空,跳過。
    //如果非第一個結點則直接執行CAS入隊操作,嘗試在尾部快速添加
    if (pred != null) {
        node.prev = pred;
        //使用CAS執行尾部結點替換,嘗試在尾部快速添加
        if (compareAndSetTail(pred, node)) {
            pred.next = node;
            return node;
        }
    }
    //如果第一次加入或者CAS操作沒有成功執行enq入隊操作
    enq(node);
    return node;
}

創建了一個Node.EXCLUSIVE類型Node結點用於封裝線程及其相關信息,其中tail是AQS的成員變量,指向隊尾(這點前面的我們分析過AQS維持的是一個雙向的鏈表結構同步隊列),如果是第一個結點,則爲tail肯定爲空,那麼將執行enq(node)操作,如果非第一個結點即tail指向不爲null,直接嘗試執行CAS操作加入隊尾,如果CAS操作失敗還是會執行enq(node),繼續看enq(node)

private Node enq(final Node node) {
    //死循環
    for (;;) {
         Node t = tail;
         //如果隊列爲null,即沒有頭結點
         if (t == null) { // Must initialize
             //創建並使用CAS設置頭結點
             if (compareAndSetHead(new Node()))
                 tail = head;
         } else {//隊尾添加新結點
             node.prev = t;
             if (compareAndSetTail(t, node)) {
                 t.next = node;
                 return t;
             }
         }
     }
    }

這個方法使用一個死循環進行CAS操作,可以解決多線程併發問題。這裏做了兩件事,一是如果還沒有初始同步隊列則創建新結點並使用compareAndSetHead設置頭結點,tail也指向head,二是隊列已存在,則將新結點node添加到隊尾。注意這兩個步驟都存在同一時間多個線程操作的可能,如果有一個線程修改head和tail成功,那麼其他線程將繼續循環,直到修改成功,這裏使用CAS原子操作進行頭結點設置和尾結點tail替換可以保證線程安全,從這裏也可以看出head結點本身不存在任何數據,它只是作爲一個牽頭結點,而tail永遠指向尾部結點(前提是隊列不爲null)。

添加到同步隊列後,結點就會進入一個自旋過程,即每個結點都在觀察時機待條件滿足獲取同步狀態,然後從同步隊列退出並結束自旋,回到之前的acquire()方法,自旋過程是在acquireQueued(addWaiter(Node.EXCLUSIVE), arg))方法中執行的,代碼如下

final boolean acquireQueued(final Node node, int arg) {
    boolean failed = true;
    try {
        boolean interrupted = false;
        //自旋,死循環
        for (;;) {
            //獲取前驅結點
            final Node p = node.predecessor();
            當且僅當p爲頭結點才嘗試獲取同步狀態
            if (p == head && tryAcquire(arg)) {
                //將node設置爲頭結點
                setHead(node);
                //清空原來頭結點的引用便於GC
                p.next = null; // help GC
                failed = false;
                return interrupted;
            }
            //如果前驅結點不是head,判斷是否掛起線程
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        if (failed)
            //最終都沒能獲取同步狀態,結束該線程的請求
            cancelAcquire(node);
    }
}

當前線程在自旋(死循環)中獲取同步狀態,當且僅當前驅結點爲頭結點才嘗試獲取同步狀態,這符合FIFO的規則,即先進先出,其次head是當前獲取同步狀態的線程結點,只有當head釋放同步狀態喚醒後繼結點,後繼結點纔有可能獲取到同步狀態,因此後繼結點在其前繼結點爲head時,才進行嘗試獲取同步狀態,其他時刻將被掛起。進入if語句後調用setHead(node)方法,將當前線程結點設置爲head

//設置爲頭結點
private void setHead(Node node) {
        head = node;
        //清空結點數據
        node.thread = null;
        node.prev = null;
}

設置爲node結點被設置爲head後,其thread信息和前驅結點將被清空,因爲該線程已獲取到同步狀態(鎖),正在執行了,也就沒有必要存儲相關信息了,head只有保存指向後繼結點的指針即可,便於head結點釋放同步狀態後喚醒後繼結點,執行結果如下圖

從圖可知更新head結點的指向,將後繼結點的線程喚醒並獲取同步狀態,調用setHead(node)將其替換爲head結點,清除相關無用數據。當然如果前驅結點不是head,那麼執行如下

//如果前驅結點不是head,判斷是否掛起線程
if (shouldParkAfterFailedAcquire(p, node) &&parkAndCheckInterrupt())

      interrupted = true;
}

private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
        //獲取當前結點的等待狀態
        int ws = pred.waitStatus;
        //如果爲等待喚醒(SIGNAL)狀態則返回true
        if (ws == Node.SIGNAL)
            return true;
        //如果ws>0 則說明是結束狀態,
        //遍歷前驅結點直到找到沒有結束狀態的結點
        if (ws > 0) {
            do {
                node.prev = pred = pred.prev;
            } while (pred.waitStatus > 0);
            pred.next = node;
        } else {
            //如果ws小於0又不是SIGNAL狀態,
            //則將其設置爲SIGNAL狀態,代表該結點的線程正在等待喚醒。
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
        return false;
    }

private final boolean parkAndCheckInterrupt() {
        //將當前線程掛起
        LockSupport.park(this);
        //獲取線程中斷狀態,interrupted()是判斷當前中斷狀態,
        //並非中斷線程,因此可能true也可能false,並返回
        return Thread.interrupted();
}

shouldParkAfterFailedAcquire()方法的作用是判斷當前結點的前驅結點是否爲SIGNAL狀態(即等待喚醒狀態),如果是則返回true。如果結點的ws爲CANCELLED狀態(值爲1>0),即結束狀態,則說明該前驅結點已沒有用應該從同步隊列移除,執行while循環,直到尋找到非CANCELLED狀態的結點。倘若前驅結點的ws值不爲CANCELLED,也不爲SIGNAL(當從Condition的條件等待隊列轉移到同步隊列時,結點狀態爲CONDITION因此需要轉換爲SIGNAL),那麼將其轉換爲SIGNAL狀態,等待被喚醒。
shouldParkAfterFailedAcquire()方法返回true,即前驅結點爲SIGNAL狀態同時又不是head結點,那麼使用parkAndCheckInterrupt()方法掛起當前線程,稱爲WAITING狀態,需要等待一個unpark()操作來喚醒它,到此ReetrantLock內部間接通過AQS的FIFO的同步隊列就完成了lock()操作,這裏我們總結成邏輯流程圖

關於獲取鎖的操作,這裏看看另外一種可中斷的獲取方式,即調用ReentrantLock類的lockInterruptibly()或者tryLock()方法,最終它們都間接調用到doAcquireInterruptibly()

 private void doAcquireInterruptibly(int arg)
        throws InterruptedException {
        final Node node = addWaiter(Node.EXCLUSIVE);
        boolean failed = true;
        try {
            for (;;) {
                final Node p = node.predecessor();
                if (p == head && tryAcquire(arg)) {
                    setHead(node);
                    p.next = null; // help GC
                    failed = false;
                    return;
                }
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    //直接拋異常,中斷線程的同步狀態請求
                    throw new InterruptedException();
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

最大的不同是

if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
     //直接拋異常,中斷線程的同步狀態請求
       throw new InterruptedException();

檢測到線程的中斷操作後,直接拋出異常,從而中斷線程的同步狀態請求,移除同步隊列,ok~,加鎖流程到此。下面接着看unlock()操作

//ReentrantLock類的unlock
public void unlock() {
    sync.release(1);
}

//AQS類的release()方法
public final boolean release(int arg) {
    //嘗試釋放鎖
    if (tryRelease(arg)) {

        Node h = head;
        if (h != null && h.waitStatus != 0)
            //喚醒後繼結點的線程
            unparkSuccessor(h);
        return true;
    }
    return false;
}

//ReentrantLock類中的內部類Sync實現的tryRelease(int releases) 
protected final boolean tryRelease(int releases) {

      int c = getState() - releases;
      if (Thread.currentThread() != getExclusiveOwnerThread())
          throw new IllegalMonitorStateException();
      boolean free = false;
      //判斷狀態是否爲0,如果是則說明已釋放同步狀態
      if (c == 0) {
          free = true;
          //設置Owner爲null
          setExclusiveOwnerThread(null);
      }
      //設置更新同步狀態
      setState(c);
      return free;
  }

釋放同步狀態的操作相對簡單些,tryRelease(int releases)方法是ReentrantLock類中內部類自己實現的,因爲AQS對於釋放鎖並沒有提供具體實現,必須由子類自己實現。釋放同步狀態後會使用unparkSuccessor(h)喚醒後繼結點的線程,這裏看看unparkSuccessor(h)

private void unparkSuccessor(Node node) {
    //這裏,node一般爲當前線程所在的結點。
    int ws = node.waitStatus;
    if (ws < 0)//置零當前線程所在的結點狀態,允許失敗。
        compareAndSetWaitStatus(node, ws, 0);

    Node s = node.next;//找到下一個需要喚醒的結點s
    if (s == null || s.waitStatus > 0) {//如果爲空或已取消
        s = null;
        for (Node t = tail; t != null && t != node; t = t.prev)
            if (t.waitStatus <= 0)//從這裏可以看出,<=0的結點,都是還有效的結點。
                s = t;
    }
    if (s != null)
        LockSupport.unpark(s.thread);//喚醒
}

從代碼執行操作來看,這裏主要作用是用unpark()喚醒同步隊列中最前邊未放棄線程(也就是狀態爲CANCELLED的線程結點s)。此時,回憶前面分析進入自旋的函數acquireQueued(),s結點的線程被喚醒後,會進入acquireQueued()函數的if (p == head && tryAcquire(arg))的判斷,如果p!=head也不會有影響,因爲它會執行shouldParkAfterFailedAcquire(),由於s通過unparkSuccessor()操作後已是同步隊列中最前邊未放棄的線程結點,那麼通過shouldParkAfterFailedAcquire()內部對結點狀態的調整,s也必然會成爲head的next結點,因此再次自旋時p==head就成立了,然後s把自己設置成head結點,表示自己已經獲取到資源了,最終acquire()也返回了,這就是獨佔鎖釋放的過程。
ok~,關於獨佔模式的加鎖和釋放鎖的過程到這就分析完,總之呢,在AQS同步器中維護着一個同步隊列,當線程獲取同步狀態失敗後,將會被封裝成Node結點,加入到同步隊列中並進行自旋操作,噹噹前線程結點的前驅結點爲head時,將嘗試獲取同步狀態,獲取成功將自己設置爲head結點。在釋放同步狀態時,則通過調用子類(ReetrantLock中的Sync內部類)的tryRelease(int releases)方法釋放同步狀態,釋放成功則喚醒後繼結點的線程。

ReetrantLock中公平鎖

瞭解完ReetrantLock中非公平鎖的實現後,我們再來看看公平鎖。與非公平鎖不同的是,在獲取鎖的時,公平鎖的獲取順序是完全遵循時間上的FIFO規則,也就是說先請求的線程一定會先獲取鎖,後來的線程肯定需要排隊,這點與前面我們分析非公平鎖的nonfairTryAcquire(int acquires)方法實現有鎖不同,下面是公平鎖中tryAcquire()方法的實現

//公平鎖FairSync類中的實現
protected final boolean tryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState();
            if (c == 0) {
            //注意!!這裏先判斷同步隊列是否存在結點
                if (!hasQueuedPredecessors() &&
                    compareAndSetState(0, acquires)) {
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
            else if (current == getExclusiveOwnerThread()) {
                int nextc = c + acquires;
                if (nextc < 0)
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            }
            return false;
        }

該方法與nonfairTryAcquire(int acquires)方法唯一的不同是在使用CAS設置嘗試設置state值前,調用了hasQueuedPredecessors()判斷同步隊列是否存在結點,如果存在必須先執行完同步隊列中結點的線程,當前線程進入等待狀態。這就是非公平鎖與公平鎖最大的區別,即公平鎖在線程請求到來時先會判斷同步隊列是否存在結點,如果存在先執行同步隊列中的結點線程,當前線程將封裝成node加入同步隊列等待。而非公平鎖呢,當線程請求到來時,不管同步隊列是否存在線程結點,直接嘗試獲取同步狀態,獲取成功直接訪問共享資源,但請注意在絕大多數情況下,非公平鎖纔是我們理想的選擇,畢竟從效率上來說非公平鎖總是勝於公平鎖。
    以上便是ReentrantLock的內部實現原理,這裏我們簡單進行小結,重入鎖ReentrantLock,是一個基於AQS併發框架的併發控制類,其內部實現了3個類,分別是Sync、NoFairSync以及FairSync類,其中Sync繼承自AQS,實現了釋放鎖的模板方法tryRelease(int),而NoFairSync和FairSync都繼承自Sync,實現各種獲取鎖的方法tryAcquire(int)。ReentrantLock的所有方法實現幾乎都間接調用了這3個類,因此當我們在使用ReentrantLock時,大部分使用都是在間接調用AQS同步器中的方法,這就是ReentrantLock的內部實現原理,最後給出張類圖結構

關於synchronized 與ReentrantLock

在JDK 1.6之後,虛擬機對於synchronized關鍵字進行整體優化後,在性能上synchronized與ReentrantLock已沒有明顯差距,因此在使用選擇上,需要根據場景而定,大部分情況下我們依然建議是synchronized關鍵字,原因之一是使用方便語義清晰,二是性能上虛擬機已爲我們自動優化。而ReentrantLock提供了多樣化的同步特性,如超時獲取鎖、可以被中斷獲取鎖(synchronized的同步是不能中斷的)、等待喚醒機制的多個條件變量(Condition)等,因此當我們確實需要使用到這些功能是,可以選擇ReentrantLock

神奇的Condition

關於Condition接口

在併發編程中,每個Java對象都存在一組監視器方法,如wait()notify()以及notifyAll()方法,通過這些方法,我們可以實現線程間通信與協作(也稱爲等待喚醒機制),如生產者-消費者模式,而且這些方法必須配合着synchronized關鍵字使用,關於這點,如果想有更深入的理解,可觀看博主另外一篇博文【 深入理解Java併發之synchronized實現原理】,與synchronized的等待喚醒機制相比Condition具有更多的靈活性以及精確性,這是因爲notify()在喚醒線程時是隨機(同一個鎖),而Condition則可通過多個Condition實例對象建立更加精細的線程控制,也就帶來了更多靈活性了,我們可以簡單理解爲以下兩點

  • 通過Condition能夠精細的控制多線程的休眠與喚醒。

  • 對於一個鎖,我們可以爲多個線程間建立不同的Condition。

Condition是一個接口類,其主要方法如下:

public interface Condition {

 /**
  * 使當前線程進入等待狀態直到被通知(signal)或中斷
  * 當其他線程調用singal()或singalAll()方法時,該線程將被喚醒
  * 當其他線程調用interrupt()方法中斷當前線程
  * await()相當於synchronized等待喚醒機制中的wait()方法
  */
 void await() throws InterruptedException;

 //當前線程進入等待狀態,直到被喚醒,該方法不響應中斷要求
 void awaitUninterruptibly();

 //調用該方法,當前線程進入等待狀態,直到被喚醒或被中斷或超時
 //其中nanosTimeout指的等待超時時間,單位納秒
 long awaitNanos(long nanosTimeout) throws InterruptedException;

  //同awaitNanos,但可以指明時間單位
  boolean await(long time, TimeUnit unit) throws InterruptedException;

 //調用該方法當前線程進入等待狀態,直到被喚醒、中斷或到達某個時
 //間期限(deadline),如果沒到指定時間就被喚醒,返回true,其他情況返回false
  boolean awaitUntil(Date deadline) throws InterruptedException;

 //喚醒一個等待在Condition上的線程,該線程從等待方法返回前必須
 //獲取與Condition相關聯的鎖,功能與notify()相同
  void signal();

 //喚醒所有等待在Condition上的線程,該線程從等待方法返回前必須
 //獲取與Condition相關聯的鎖,功能與notifyAll()相同
  void signalAll();
}

關於Condition的實現類是AQS的內部類ConditionObject,關於這點我們稍後分析,這裏先來看一個Condition的使用案例,即經典消費者生產者模式

Condition的使用案例-生產者消費者模式

這裏我們通過一個賣烤鴨的案例來演示多生產多消費者的案例,該場景中存在兩條生產線程t1和t2,用於生產烤鴨,也存在兩條消費線程t3,t4用於消費烤鴨,4條線程同時執行,需要保證只有在生產線程產生烤鴨後,消費線程才能消費,否則只能等待,直到生產線程產生烤鴨後喚醒消費線程,注意烤鴨不能重複消費。ResourceByCondition類中定義product()和consume()兩個方法,分別用於生產烤鴨和消費烤鴨,並且定義ReentrantLock鎖,用於控制product()和consume()的併發,由於必須在烤鴨生成完成後消費線程才能消費烤鴨,否則只能等待,因此這裏定義兩組Condition對象,分別是producer_con和consumer_con,前者擁有控制生產線程,後者擁有控制消費線程,這裏我們使用一個標誌flag來控制是否有烤鴨,當flag爲true時,代表烤鴨生成完畢,生產線程必須進入等待狀態同時喚醒消費線程進行消費,消費線程消費完畢後將flag設置爲false,代表烤鴨消費完成,進入等待狀態,同時喚醒生產線程生產烤鴨,具體代碼如下

package com.zejian.concurrencys;

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

/**
 * Created by zejian on 2017/7/22.
 * Blog : http://blog.csdn.net/javazejian [原文地址,請尊重原創]
 */
public class ResourceByCondition {
    private String name;
    private int count = 1;
    private boolean flag = false;

    //創建一個鎖對象。
    Lock lock = new ReentrantLock();

    //通過已有的鎖獲取兩組監視器,一組監視生產者,一組監視消費者。
    Condition producer_con = lock.newCondition();
    Condition consumer_con = lock.newCondition();

    /**
     * 生產
     * @param name
     */
    public  void product(String name)
    {
        lock.lock();
        try
        {
            while(flag){
                try{producer_con.await();}catch(InterruptedException e){}
            }
            this.name = name + count;
            count++;
            System.out.println(Thread.currentThread().getName()+"...生產者5.0..."+this.name);
            flag = true;
            consumer_con.signal();//直接喚醒消費線程
        }
        finally
        {
            lock.unlock();
        }
    }

    /**
     * 消費
     */
    public  void consume()
    {
        lock.lock();
        try
        {
            while(!flag){
                try{consumer_con.await();}catch(InterruptedException e){}
            }
            System.out.println(Thread.currentThread().getName()+"...消費者.5.0......."+this.name);//消費烤鴨1
            flag = false;
            producer_con.signal();//直接喚醒生產線程
        }
        finally
        {
            lock.unlock();
        }
    }
}

執行代碼

package com.zejian.concurrencys;
/**
 * Created by zejian on 2017/7/22.
 * Blog : http://blog.csdn.net/javazejian [原文地址,請尊重原創]
 */
public class Mutil_Producer_ConsumerByCondition {

    public static void main(String[] args) {
        ResourceByCondition r = new ResourceByCondition();
        Mutil_Producer pro = new Mutil_Producer(r);
        Mutil_Consumer con = new Mutil_Consumer(r);
        //生產者線程
        Thread t0 = new Thread(pro);
        Thread t1 = new Thread(pro);
        //消費者線程
        Thread t2 = new Thread(con);
        Thread t3 = new Thread(con);
        //啓動線程
        t0.start();
        t1.start();
        t2.start();
        t3.start();
    }
}

/**
 * @decrition 生產者線程
 */
class Mutil_Producer implements Runnable {
    private ResourceByCondition r;

    Mutil_Producer(ResourceByCondition r) {
        this.r = r;
    }

    public void run() {
        while (true) {
            r.product("北京烤鴨");
        }
    }
}

/**
 * @decrition 消費者線程
 */
class Mutil_Consumer implements Runnable {
    private ResourceByCondition r;

    Mutil_Consumer(ResourceByCondition r) {
        this.r = r;
    }

    public void run() {
        while (true) {
            r.consume();
        }
    }
}

正如代碼所示,我們通過兩者Condition對象單獨控制消費線程與生產消費,這樣可以避免消費線程在喚醒線程時喚醒的還是消費線程,如果是通過synchronized的等待喚醒機制實現的話,就可能無法避免這種情況,畢竟同一個鎖,對於synchronized關鍵字來說只能有一組等待喚醒隊列,而不能像Condition一樣,同一個鎖擁有多個等待隊列。synchronized的實現方案如下,

public class KaoYaResource {

    private String name;
    private int count = 1;//烤鴨的初始數量
    private boolean flag = false;//判斷是否有需要線程等待的標誌
    /**
     * 生產烤鴨
     */
    public synchronized void product(String name){
        while(flag){
            //此時有烤鴨,等待
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        this.name=name+count;//設置烤鴨的名稱
        count++;
        System.out.println(Thread.currentThread().getName()+"...生產者..."+this.name);
        flag=true;//有烤鴨後改變標誌
        notifyAll();//通知消費線程可以消費了
    }

    /**
     * 消費烤鴨
     */
    public synchronized void consume(){
        while(!flag){//如果沒有烤鴨就等待
            try{this.wait();}catch(InterruptedException e){}
        }
        System.out.println(Thread.currentThread().getName()+"...消費者........"+this.name);//消費烤鴨1
        flag = false;
        notifyAll();//通知生產者生產烤鴨
    }
}

如上代碼,在調用notify()或者 notifyAll()方法時,由於等待隊列中同時存在生產者線程和消費者線程,所以我們並不能保證被喚醒的到底是消費者線程還是生產者線程,而Codition則可以避免這種情況。嗯,瞭解完Condition的使用方式後,下面我們將進一步探討Condition背後的實現機制

Condition的實現原理

Condition的具體實現類是AQS的內部類ConditionObject,前面我們分析過AQS中存在兩種隊列,一種是同步隊列,一種是等待隊列,而等待隊列就相對於Condition而言的。注意在使用Condition前必須獲得鎖,同時在Condition的等待隊列上的結點與前面同步隊列的結點是同一個類即Node,其結點的waitStatus的值爲CONDITION。在實現類ConditionObject中有兩個結點分別是firstWaiter和lastWaiter,firstWaiter代表等待隊列第一個等待結點,lastWaiter代表等待隊列最後一個等待結點,如下

 public class ConditionObject implements Condition, java.io.Serializable {
    //等待隊列第一個等待結點
    private transient Node firstWaiter;
    //等待隊列最後一個等待結點
    private transient Node lastWaiter;
    //省略其他代碼.......
}

每個Condition都對應着一個等待隊列,也就是說如果一個鎖上創建了多個Condition對象,那麼也就存在多個等待隊列。等待隊列是一個FIFO的隊列,在隊列中每一個節點都包含了一個線程的引用,而該線程就是Condition對象上等待的線程。當一個線程調用了await()相關的方法,那麼該線程將會釋放鎖,並構建一個Node節點封裝當前線程的相關信息加入到等待隊列中進行等待,直到被喚醒、中斷、超時才從隊列中移出。Condition中的等待隊列模型如下

正如圖所示,Node節點的數據結構,在等待隊列中使用的變量與同步隊列是不同的,Condtion中等待隊列的結點只有直接指向的後繼結點並沒有指明前驅結點,而且使用的變量是nextWaiter而不是next,這點我們在前面分析結點Node的數據結構時講過。firstWaiter指向等待隊列的頭結點,lastWaiter指向等待隊列的尾結點,等待隊列中結點的狀態只有兩種即CANCELLED和CONDITION,前者表示線程已結束需要從等待隊列中移除,後者表示條件結點等待被喚醒。再次強調每個Codition對象對於一個等待隊列,也就是說AQS中只能存在一個同步隊列,但可擁有多個等待隊列。下面從代碼層面看看被調用await()方法(其他await()實現原理類似)的線程是如何加入等待隊列的,而又是如何從等待隊列中被喚醒的

public final void await() throws InterruptedException {
      //判斷線程是否被中斷
      if (Thread.interrupted())
          throw new InterruptedException();
      //創建新結點加入等待隊列並返回
      Node node = addConditionWaiter();
      //釋放當前線程鎖即釋放同步狀態
      int savedState = fullyRelease(node);
      int interruptMode = 0;
      //判斷結點是否同步隊列(SyncQueue)中,即是否被喚醒
      while (!isOnSyncQueue(node)) {
          //掛起線程
          LockSupport.park(this);
          //判斷是否被中斷喚醒,如果是退出循環。
          if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
              break;
      }
      //被喚醒後執行自旋操作爭取獲得鎖,同時判斷線程是否被中斷
      if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
          interruptMode = REINTERRUPT;
       // clean up if cancelled
      if (node.nextWaiter != null) 
          //清理等待隊列中不爲CONDITION狀態的結點
          unlinkCancelledWaiters();
      if (interruptMode != 0)
          reportInterruptAfterWait(interruptMode);
  }

執行addConditionWaiter()添加到等待隊列。

 private Node addConditionWaiter() {
    Node t = lastWaiter;
      // 判斷是否爲結束狀態的結點並移除
      if (t != null && t.waitStatus != Node.CONDITION) {
          unlinkCancelledWaiters();
          t = lastWaiter;
      }
      //創建新結點狀態爲CONDITION
      Node node = new Node(Thread.currentThread(), Node.CONDITION);
      //加入等待隊列
      if (t == null)
          firstWaiter = node;
      else
          t.nextWaiter = node;
      lastWaiter = node;
      return node;
        }

await()方法主要做了3件事,一是調用addConditionWaiter()方法將當前線程封裝成node結點加入等待隊列,二是調用fullyRelease(node)方法釋放同步狀態並喚醒後繼結點的線程。三是調用isOnSyncQueue(node)方法判斷結點是否在同步隊列中,注意是個while循環,如果同步隊列中沒有該結點就直接掛起該線程,需要明白的是如果線程被喚醒後就調用acquireQueued(node, savedState)執行自旋操作爭取鎖,即當前線程結點從等待隊列轉移到同步隊列並開始努力獲取鎖。

接着看看喚醒操作singal()方法

 public final void signal() {
     //判斷是否持有獨佔鎖,如果不是拋出異常
   if (!isHeldExclusively())
          throw new IllegalMonitorStateException();
      Node first = firstWaiter;
      //喚醒等待隊列第一個結點的線程
      if (first != null)
          doSignal(first);
 }

這裏signal()方法做了兩件事,一是判斷當前線程是否持有獨佔鎖,沒有就拋出異常,從這點也可以看出只有獨佔模式先採用等待隊列,而共享模式下是沒有等待隊列的,也就沒法使用Condition。二是喚醒等待隊列的第一個結點,即執行doSignal(first)

 private void doSignal(Node first) {
     do {
             //移除條件等待隊列中的第一個結點,
             //如果後繼結點爲null,那麼說沒有其他結點將尾結點也設置爲null
            if ( (firstWaiter = first.nextWaiter) == null)
                 lastWaiter = null;
             first.nextWaiter = null;
          //如果被通知節點沒有進入到同步隊列並且條件等待隊列還有不爲空的節點,則繼續循環通知後續結點
         } while (!transferForSignal(first) &&
                  (first = firstWaiter) != null);
        }

//transferForSignal方法
final boolean transferForSignal(Node node) {
    //嘗試設置喚醒結點的waitStatus爲0,即初始化狀態
    //如果設置失敗,說明當期結點node的waitStatus已不爲
    //CONDITION狀態,那麼只能是結束狀態了,因此返回false
    //返回doSignal()方法中繼續喚醒其他結點的線程,注意這裏並
    //不涉及併發問題,所以CAS操作失敗只可能是預期值不爲CONDITION,
    //而不是多線程設置導致預期值變化,畢竟操作該方法的線程是持有鎖的。
    if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
         return false;

        //加入同步隊列並返回前驅結點p
        Node p = enq(node);
        int ws = p.waitStatus;
        //判斷前驅結點是否爲結束結點(CANCELLED=1)或者在設置
        //前驅節點狀態爲Node.SIGNAL狀態失敗時,喚醒被通知節點代表的線程
        if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
            //喚醒node結點的線程
            LockSupport.unpark(node.thread);
        return true;
    }

註釋說得很明白了,這裏我們簡單整體說明一下,doSignal(first)方法中做了兩件事,從條件等待隊列移除被喚醒的節點,然後重新維護條件等待隊列的firstWaiter和lastWaiter的指向。二是將從等待隊列移除的結點加入同步隊列(在transferForSignal()方法中完成的),如果進入到同步隊列失敗並且條件等待隊列還有不爲空的節點,則繼續循環喚醒後續其他結點的線程。到此整個signal()的喚醒過程就很清晰了,即signal()被調用後,先判斷當前線程是否持有獨佔鎖,如果有,那麼喚醒當前Condition對象中等待隊列的第一個結點的線程,並從等待隊列中移除該結點,移動到同步隊列中,如果加入同步隊列失敗,那麼繼續循環喚醒等待隊列中的其他結點的線程,如果成功加入同步隊列,那麼如果其前驅結點是否已結束或者設置前驅節點狀態爲Node.SIGNAL狀態失敗,則通過LockSupport.unpark()喚醒被通知節點代表的線程,到此signal()任務完成,注意被喚醒後的線程,將從前面的await()方法中的while循環中退出,因爲此時該線程的結點已在同步隊列中,那麼while (!isOnSyncQueue(node))將不在符合循環條件,進而調用AQS的acquireQueued()方法加入獲取同步狀態的競爭中,這就是等待喚醒機制的整個流程實現原理,流程如下圖所示(注意無論是同步隊列還是等待隊列使用的Node數據結構都是同一個,不過是使用的內部變量不同罷了)

ok~,本篇先到這,關於AQS中的另一種模式即共享模式,下篇再詳聊,歡迎繼續關注。

主要參考資料
《Java併發編程的藝術》
《Java 編程思想》

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