AQS原始碼深入分析之獨佔模式-ReentrantLock鎖特性詳解

雕爺的架構之路發表於2020-11-02

本文基於JDK-8u261原始碼分析


相信大部分人知道AQS是因為ReentrantLock,ReentrantLock的底層是使用AQS來實現的。還有一部分人知道共享鎖(Semaphore/CountDownLatch/CyclicBarrier)也是由AQS來實現的。也就是說AQS中有獨佔和共享兩種模式。但你以為這就是AQS的全部了嗎?其實不然。AQS中還有第三種模式:條件佇列。像Java中的阻塞佇列(ArrayBlockingQueue、LinkedBlockingQueue等)就是由AQS中的條件佇列來實現的。而上面所說的獨佔模式和共享模式是由AQS中的CLH佇列來實現的。

所以本系列的AQS原始碼分析將會分為三篇文章來推送(獨佔模式/共享模式/條件佇列),並且會深入到每一行原始碼進行分析,希望能給你帶來一個全面的AQS認識。那麼首先本篇文章就來分析一下AQS中獨佔模式的實現吧。


1 簡介

AQS全稱AbstractQueuedSynchronizer,是一個能多執行緒訪問共享資源的同步器框架。作為Doug Lea大神設計出來的又一款優秀的併發框架,AQS的出現使得Java中終於可以有一個通用的併發處理機制。並且可以通過繼承它,實現其中的方法,以此來實現想要的獨佔模式或共享模式,抑或是阻塞佇列也可以通過AQS來很簡單地實現出來。

一些常用的併發工具類底層都是通過繼承AQS來實現的,比如:ReentrantLock、Semaphore、CountDownLatch、ArrayBlockingQueue等(這些工具類也都是Doug Lea寫的)。

Doug Lea是我們學習Java併發框架繞不開的神級人物,以下是他個人的履歷

紐約州立大學奧斯威戈分校的電腦科學教授,現任電腦科學系主任,他專門研究併發程式設計和併發資料結構的設計。他是Java Community Process執行委員會的成員,JSR 166的主席, 美國計算機協會會士,歐洲計算機領域重量級獎項達爾-尼加德獎得主

資訊來自wiki百科:https://en.wikipedia.org/wiki/Doug_Lea

扯回正題,AQS中有幾個重要的概念:

  • state:用來記錄可重入鎖的上鎖次數;

  • exclusiveOwnerThread:AQS繼承了AbstractOwnableSynchronizer,而其中有個屬性exclusiveOwnerThread,用來記錄當前獨佔鎖的執行緒是誰;

  • CLH同步佇列:FIFO雙向連結串列佇列,此CLH佇列是原CLH的變種,由原來的不斷自旋改為了阻塞機制。佇列中有頭節點和尾節點兩個指標,尾節點就是指向最後一個節點,而頭節點為了便於判斷,永遠指向一個空節點,之後才是第一個有資料的節點;

  • 條件佇列:能夠使某些執行緒一起等待某個條件具備時,才會被喚醒,喚醒後會被放到CLH佇列中重新爭奪鎖資源。

    AQS定義資源的訪問方式有兩種:

  • 獨佔模式:只有一個執行緒能夠獲取鎖,如ReentrantLock;

  • 共享模式:多個執行緒可以同時獲取到鎖,如Semaphore、CountDownLatch和CyclicBarrier。

AQS中使用到了模板方法模式,提供了一些方法供子類來實現,子類只需要實現這些方法即可,至於具體的佇列的維護就不需要關心了,AQS已經實現好了。

1.1 Node

上面所說的CLH佇列和條件佇列的節點都是AQS的一個內部類Node構造的,其中定義了一些節點的屬性:

 1  static final class Node {
 2      /**
 3       * 標記節點為共享模式
 4       */
 5      static final Node SHARED = new Node();
 6      /**
 7       * 標記節點為獨佔模式
 8       */
 9      static final Node EXCLUSIVE = null;
10      /**
11       * 標記節點是取消狀態,CLH佇列中等待超時或者被中斷的執行緒,需要從CLH佇列中去掉
12       */
13      static final int CANCELLED = 1;
14      /**
15       * 該狀態比較特殊,如果該節點的下一個節點是阻塞狀態,則該節點處於SIGNAL狀態
16       * 所以該狀態表示的是下一個節點是否是阻塞狀態,而不是表示的本節點的狀態
17       */
18      static final int SIGNAL = -1;
19      /**
20       * 該狀態的節點會被放在條件佇列中
21       */
22      static final int CONDITION = -2;
23      /**
24       * 用在共享模式中,表示節點是可以喚醒傳播的。CLH佇列此時不需要等待前一個節點釋放鎖之後,該節點再獲取鎖
25       * 共享模式下所有處於該狀態的節點都可以獲取到鎖,而這個傳播喚醒的動作就是通過標記為PROPAGATE狀態來實現
26       */
27      static final int PROPAGATE = -3;
28      /**
29       * 記錄當前節點的狀態,除了上述四種狀態外,還有一個初始狀態0
30       */
31      volatile int waitStatus;
32      /**
33       * CLH佇列中用來表示前一個節點
34       */
35      volatile Node prev;
36      /**
37       * CLH佇列中用來表示後一個節點
38       */
39      volatile Node next;
40      /**
41       * 用來記錄當前被阻塞的執行緒
42       */
43      volatile Thread thread;
44      /**
45       * 條件佇列中用來表示下一個節點
46       */
47      Node nextWaiter;
48
49      //...
50  }

1.2 CLH佇列

img

這裡需要注意的一點是,CLH佇列中的head指標永遠會指向一個空節點。如果當前節點被剔除掉,而後面的節點變成第一個節點的時候,此時就會清空該節點裡面的內容(waitStatus不會被清除),將head指標指向它。這樣做的目的是為了方便進行判斷。


2 ReentrantLock概覽

獨佔模式就是隻有一個執行緒能獲取到鎖資源,獨佔模式用ReentrantLock來舉例,ReentrantLock內部使用sync來繼承AQS,有公平鎖和非公平鎖兩種:

 1 public class ReentrantLock implements Lock, Serializable {
 2
 3    //...
 4
 5    /**
 6     * 內部呼叫AQS
 7     */
 8    private final Sync sync;
 9
10    /**
11     * 繼承AQS的同步基礎類
12     */
13    abstract static class Sync extends AbstractQueuedSynchronizer {
14        //...
15    }
16
17    /**
18     * 非公平鎖
19     */
20    static final class NonfairSync extends Sync {
21        //...
22    }
23
24    /**
25     * 公平鎖
26     */
27    static final class FairSync extends Sync {
28        //...
29    }
30
31    /**
32     * 預設建立非公平鎖物件
33     */
34    public ReentrantLock() {
35        sync = new NonfairSync();
36    }
37
38    /**
39     * 建立公平鎖或者非公平鎖
40     */
41    public ReentrantLock(boolean fair) {
42        sync = fair ? new FairSync() : new NonfairSync();
43    }
44
45    //...
46  }

公平與非公平鎖區別

AQS設計了佇列給所有未獲取到鎖的執行緒進行排隊,那為什麼選擇佇列而不使用Set或者List結構呢?因為佇列具有FIFO先入先出特性,即天然具備公平特性,因此在ReentrantLock裡才有公平與非公平這兩種特性存在。


3 非公平鎖

3.1 lock方法

ReentrantLock的非公平鎖方式下的lock方法:

  1  /**
  2   * ReentrantLock:
  3   */
  4  public void lock() {
  5    sync.lock();
  6  }
  7
  8  final void lock() {
  9    /*
 10    首先直接嘗試CAS方式加鎖,如果成功了,就將exclusiveOwnerThread設定為當前執行緒
 11    這也就是非公平鎖的含義,每一個執行緒在進行加鎖的時候,會首先嚐試加鎖,如果成功了,
 12    就不用放在CLH佇列中進行排隊阻塞了
 13     */
 14    if (compareAndSetState(0, 1))
 15      setExclusiveOwnerThread(Thread.currentThread());
 16    else
 17      //否則失敗的話就進CLH佇列中進行阻塞
 18      acquire(1);
 19  }

3.2 acquire方法

在上面的lock方法中,如果加鎖失敗了,就會進入到acquire方法中進行排隊。但首先還是會嘗試獲取一次資源:

 1  /**
 2   * AbstractQueuedSynchronizer:
 3   */
 4  public final void acquire(int arg) {
 5    //首先嚐試獲取資源,如果失敗了的話就新增一個新的獨佔節點,插入到CLH佇列尾部
 6    if (!tryAcquire(arg) &&
 7            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
 8        /*
 9        因為本方法不是響應中斷的,所以如果當前執行緒中斷後被喚醒,就在此處繼續將中斷標誌位重新置為true
10        (selfInterrupt方法內部就一句話:“Thread.currentThread().interrupt();”),而不是會拋異常
11        (需要使用者在呼叫lock方法後首先通過isInterrupted方法去進行判斷,是否應該執行接下來的業務程式碼)
12         */
13        selfInterrupt();
14  }
15
16  /**
17   * ReentrantLock:
18   * 第6行程式碼處:
19   * 嘗試獲取資源
20   */
21  protected final boolean tryAcquire(int acquires) {
22    return nonfairTryAcquire(acquires);
23  }
24
25  final boolean nonfairTryAcquire(int acquires) {
26    //acquires = 1
27    final Thread current = Thread.currentThread();
28    int c = getState();
29    //如果當前沒有加鎖的話
30    if (c == 0) {
31        //嘗試CAS方式去修改state為1
32        if (compareAndSetState(0, acquires)) {
33            //設定當前獨佔鎖擁有者為當前執行緒
34            setExclusiveOwnerThread(current);
35            return true;
36        }
37    }
38    //當前state不為0,則判斷當前執行緒是否是之前加上鎖的執行緒
39    else if (current == getExclusiveOwnerThread()) {
40        //如果是的話,說明此時是可重入鎖,將state+1
41        int nextc = c + acquires;
42        //如果+1之後為負數,說明此時資料溢位了,丟擲Error
43        if (nextc < 0)
44            throw new Error("Maximum lock count exceeded");
45        setState(nextc);
46        return true;
47    }
48    return false;
49  }

3.3 addWaiter方法

如果tryAcquire方法還是獲取不到資源,就會呼叫addWaiter方法來在CLH佇列中新增一個節點:

 1  /**
 2   * AbstractQueuedSynchronizer:
 3   * 在CLH佇列中新增一個新的獨佔尾節點
 4   */
 5  private Node addWaiter(Node mode) {
 6    //把當前執行緒構建為一個新的節點
 7    Node node = new Node(Thread.currentThread(), mode);
 8    Node pred = tail;
 9    //判斷當前尾節點是否為null?不為null說明此時佇列中有節點
10    if (pred != null) {
11        //把當前節點用尾插的方式來插入
12        node.prev = pred;
13        //CAS的方式將尾節點指向當前節點
14        if (compareAndSetTail(pred, node)) {
15            pred.next = node;
16            return node;
17        }
18    }
19    //如果佇列為空,將佇列初始化後插入當前節點
20    enq(node);
21    return node;
22  }
23
24  private Node enq(final Node node) {
25    /*
26    高併發情景下會有很多的CAS失敗操作,而下面的死迴圈確保節點一定要插進佇列中。上面的程式碼和
27    enq方法中的程式碼是類似的,也就是說上面操作是為了做快速修改,如果失敗了,在enq方法中做兜底
28     */
29    for (; ;) {
30        Node t = tail;
31        //如果尾節點為null,說明此時CLH佇列為空,需要初始化佇列
32        if (t == null) {
33            //建立一個空的Node節點,並將頭節點CAS指向它
34            if (compareAndSetHead(new Node()))
35                //同時將尾節點也指向這個新的節點
36                tail = head;
37        } else {
38            //如果CLH佇列此時不為空,則像之前一樣用尾插的方式插入該節點
39            node.prev = t;
40            if (compareAndSetTail(t, node)) {
41                t.next = node;
42                return t;
43            }
44        }
45    }
46  }

3.4 acquireQueued方法

本方法是AQS中的核心方法,需要特別注意:

  1  /**
  2   * AbstractQueuedSynchronizer:
  3   * 注意:本方法是整個AQS的精髓所在,完成了頭節點嘗試獲取鎖資源和其他節點被阻塞的全部過程
  4   */
  5  final boolean acquireQueued(final Node node, int arg) {
  6    boolean failed = true;
  7    try {
  8        boolean interrupted = false;
  9        for (; ; ) {
 10            //獲取當前節點的前一個節點
 11            final Node p = node.predecessor();
 12            /*
 13            如果前一個節點是頭節點,才可以嘗試獲取資源,也就是實際上的CLH佇列中的第一個節點
 14            佇列中只有第一個節點才有資格去嘗試獲取鎖資源(FIFO),如果獲取到了就不用被阻塞了
 15            獲取到了說明在此刻,之前的資源已經被釋放了
 16             */
 17            if (p == head && tryAcquire(arg)) {
 18                /*
 19                頭指標指向當前節點,意味著該節點將變成一個空節點(頭節點永遠會指向一個空節點)
 20                因為在上一行的tryAcquire方法已經成功的情況下,就可以釋放CLH佇列中的該節點了
 21                 */
 22                setHead(node);
 23                //斷開前一個節點的next指標,這樣它就成為了一個孤立節點,等待被GC
 24                p.next = null;
 25                failed = false;
 26                return interrupted;
 27            }
 28            /*
 29            走到這裡說明要麼前一個節點不是head節點,要麼是head節點但是嘗試加鎖失敗。此時將佇列中當前
 30            節點之前的一些CANCELLED狀態的節點剔除;前一個節點狀態如果為SIGNAL時,就會阻塞當前執行緒
 31            這裡的parkAndCheckInterrupt阻塞操作是很有意義的。因為如果不阻塞的話,那麼獲取不到資源的
 32            執行緒可能會在這個死迴圈裡面一直執行,會一直佔用CPU資源
 33             */
 34            if (shouldParkAfterFailedAcquire(p, node) &&
 35                    parkAndCheckInterrupt())
 36                //只是記錄一個標誌位而已,不會丟擲InterruptedException異常。也就是說不會響應中斷
 37                interrupted = true;
 38        }
 39    } finally {
 40        if (failed)
 41            //如果tryAcquire方法中state+1溢位了,就會取消當前執行緒獲取鎖資源的請求
 42            cancelAcquire(node);
 43    }
 44  }
 45
 46  /**
 47   * 第22行程式碼處:
 48   * 將node節點置為新的head節點,同時將其中的thread和prev屬性置空
 49   * (注意:這裡並不會清空waitStatus值)
 50   */
 51  private void setHead(Node node) {
 52    head = node;
 53    node.thread = null;
 54    node.prev = null;
 55  }
 56
 57  /**
 58   * 第34行程式碼處:
 59   */
 60  private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
 61    int ws = pred.waitStatus;
 62    if (ws == Node.SIGNAL)
 63        //如果前一個節點的狀態是SIGNAL,意味著當前節點可以被安全地阻塞
 64        return true;
 65    if (ws > 0) {
 66        /*
 67        從該節點往前尋找一個不是CANCELLED狀態的節點(也就是處於正常阻塞狀態的節點),
 68        遍歷過程中如果遇到了CANCELLED節點,會被剔除出CLH佇列等待GC
 69         */
 70        do {
 71            node.prev = pred = pred.prev;
 72        } while (pred.waitStatus > 0);
 73        pred.next = node;
 74    } else {
 75        /*
 76        如果前一個節點的狀態是初始狀態0或者是傳播狀態PROPAGATE時,CAS去修改其狀態為SIGNAL,
 77        因為當前節點最後是要被阻塞的,所以前一個節點的狀態必須改為SIGNAL
 78        走到這裡最後會返回false,因為外面還有一個死迴圈,如果最後還能跳到這個方法裡面的話,
 79        如果之前CAS修改成功的話就會直接走進第一個if條件裡面,返回true。然後當前執行緒被阻塞
 80        CAS失敗的話會再次進入到該分支中做修改
 81         */
 82        compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
 83    }
 84    return false;
 85  }
 86
 87  /**
 88   * 第35行程式碼處:
 89   * 阻塞當前節點,後續該節點如果被unpark喚醒的時候,會從第97行程式碼處喚醒往下執行,返回false
 90   * 可能執行緒在等待的時候會被中斷喚醒,本方法就返回了true。這個時候該執行緒就會處於一種不正確的狀態
 91   * 返回回去後會在第37行程式碼處設定中斷位為true,並最終返回回去。注意到下面的第110行程式碼處使用的是
 92   * Thread.interrupted方法,也就是在返回true之後會清空中斷狀態,所以需要在上面的acquire方法
 93   * 中、呼叫selfInterrupt方法裡面的interrupt方法來將中斷標誌位重新置為true
 94   */
 95  private final boolean parkAndCheckInterrupt() {
 96    //當前執行緒會被阻塞到這行程式碼處,停止往下執行,等待unpark喚醒
 97    LockSupport.park(this);
 98    /*
 99    通過上面的解釋,可能會覺得下面的Thread.interrupted方法有點多餘,需要清除中斷標誌位,最後
100    還會將中斷標誌位重新置為true。那麼此時為什麼不直接呼叫isInterrupted方法呢?不用清除中斷標
101    志位就行了啊?其實這裡使用Thread.interrupted方法是有原因的:LockSupport.park的實現會呼叫
102    native方法,通過檢視底層的HotSpot原始碼中的park方法可知:如果在呼叫park方法時發現當前中斷標
103    志位已經為true了,此時就會直接return退出本方法了(同時不會清除中斷標誌位),也就不會再進
104    行後續的掛起執行緒的操作了。也就是說,如果是中斷喚醒,假如沒有這裡的Thread.interrupted方法
105    來清除中斷標誌位,那麼可能下一次加鎖失敗還是會走進當前park方法,而此時的中斷標誌位仍然為
106    true。但是如上面所說,進入park方法中並不會被阻塞,也就是此時的park方法會失效,會不斷在
107    acquireQueued方法中自旋,造成CPU飆高的現象出現。所以這裡的Thread.interrupted方法清除中斷
108    標誌位是為了讓後續呼叫的park方法能繼續被成功阻塞住
109     */
110    return Thread.interrupted();
111  }

3.5 cancelAcquire方法

當出現異常的時候,就會呼叫cancelAcquire方法來處理異常,同時還有一些其他的收尾工作。其中很重要的一點是:如果是需要喚醒的節點發生了異常,那麼此時需要喚醒下一個節點,以此來保證喚醒動作能夠一直傳播下去。

 1  /**
 2   * AbstractQueuedSynchronizer:
 3   * 取消當前執行緒獲取鎖資源的請求,並完成一些其他的收尾工作
 4   */
 5  private void cancelAcquire(Node node) {
 6    //非空校驗
 7    if (node == null)
 8        return;
 9
10    //節點裡面的執行緒清空
11    node.thread = null;
12
13    /*
14    從該節點往前尋找一個不是CANCELLED狀態的節點(也就是處於正常阻塞狀態的節點),
15    相當於在退出前再做次清理工作。遍歷過程中如果遇到了CANCELLED節點,會被剔除出
16    CLH佇列等待GC
17    這裡的實現邏輯是和shouldParkAfterFailedAcquire方法中是類似的,但是有一點
18    不同的是:這裡並沒有pred.next = node,而是延遲到了後面的CAS操作中
19     */
20    Node pred = node.prev;
21    while (pred.waitStatus > 0)
22        node.prev = pred = pred.prev;
23
24    /*
25    如果上面遍歷時有CANCELLED節點,predNext就指向pred節點的下一個CANCELLED節點
26    如果上面遍歷時沒有CANCELLED節點,predNext就指向自己
27     */
28    Node predNext = pred.next;
29
30    /*
31    將狀態改為CANCELLED,也就是在取消獲取鎖資源。這裡不用CAS來改狀態是可以的,
32    因為改的是CANCELLED狀態,其他節點遇到CANCELLED節點是會跳過的
33     */
34    node.waitStatus = Node.CANCELLED;
35
36    if (node == tail && compareAndSetTail(node, pred)) {
37        //如果當前節點是最後一個節點的時候,就剔除當前節點,將tail指標指向前一個節點
38        compareAndSetNext(pred, predNext, null);
39    } else {
40        int ws;
41        //走到這裡說明當前節點不是最後一個節點
42        if (pred != head &&
43                ((ws = pred.waitStatus) == Node.SIGNAL ||
44                        (ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) &&
45                pred.thread != null) {
46            /*
47            如果head指標指向的不是pred節點,並且前一個節點是SIGNAL狀態(或者可以設定為SIGNAL狀態),
48            並且前一個節點的thread沒被清空的話,那麼只需要將pred節點和當前節點的後面一個節點連線起來就行了
49             */
50            Node next = node.next;
51            if (next != null && next.waitStatus <= 0)
52                /*
53                這裡只是設定了pred節點的next指標,而沒有設定next.prev = pred。但無妨,在後續的操作中,
54                如果能走到shouldParkAfterFailedAcquire方法中,會再去修正prev指標的
55                 */
56                compareAndSetNext(pred, predNext, next);
57        } else {
58            /*
59            而如果head指標指向的是pred節點(或者pred節點的thread是為null的),那麼就去喚醒當前節點的
60            下一個可以被喚醒的節點,以保證即使是在發生異常的時候,CLH佇列中的節點也可以一直被喚醒下去
61            當然,如果前一個節點本身就是SIGNAL狀態,也是需要喚醒下一個節點的
62             */
63            unparkSuccessor(node);
64        }
65
66        /*
67        node.next指向自己,斷開該節點,同時要保證next指標一定要有值,
68        因為後續在條件佇列的isOnSyncQueue方法中會判斷節點是否在CLH佇列中
69        其中有一條就是以判斷node.next是否為null為準則,如果不為null,就說明
70        該節點還在CLH佇列中
71         */
72        node.next = node;
73    }
74  }

3.6 unparkSuccessor方法

不光上面的cancelAcquire方法會呼叫到本方法,unlock方法中也會呼叫本方法來喚醒下一個節點:

 1  /**
 2   * AbstractQueuedSynchronizer:
 3   * 喚醒下一個可以被喚醒的節點
 4   */
 5  private void unparkSuccessor(Node node) {
 6    int ws = node.waitStatus;
 7    /*
 8    如果當前節點狀態是SIGNAL或者PROPAGATE,將其CAS設定為初始狀態0
 9    因為後續會喚醒第一個被阻塞的節點,所以這裡節點的狀態如果還是SIGNAL就不正確了,
10    因為SIGNAL表示的是下一個節點是阻塞狀態 
11     */
12    if (ws < 0)
13        compareAndSetWaitStatus(node, ws, 0);
14
15    //s是當前節點的下一個節點
16    Node s = node.next;
17    //如果下一個節點為null,或者狀態為CANCELLED
18    if (s == null || s.waitStatus > 0) {
19        s = null;
20        //從CLH佇列的尾節點向前遍歷到該節點為止,找到該節點往後第一個處於正常阻塞狀態的節點
21       for (Node t = tail; t != null && t != node; t = t.prev)
22            if (t.waitStatus <= 0)
23                s = t;
24    }
25    //如果找到了或者遍歷之前的下一個節點本身就處於正常阻塞狀態的話,就喚醒它
26    if (s != null)
27        LockSupport.unpark(s.thread);
28  }

3.7 unlock方法

ReentrantLock的unlock方法:

 1  /**
 2   * ReentrantLock:
 3   */
 4  public void unlock() {
 5    sync.release(1);
 6  }
 7
 8  /**
 9   * AbstractQueuedSynchronizer:
10   */
11  public final boolean release(int arg) {
12    //釋放一次鎖,如果沒有可重入鎖的話,就進入到下面的if條件中
13    if (tryRelease(arg)) {
14        Node h = head;
15        /*
16        如果頭節點存在且下一個節點處於阻塞狀態的時候就喚醒下一個節點
17        因為在之前加鎖方法中的shouldParkAfterFailedAcquire方法中,會將前一個節點的狀態置為SIGNAL
18        所以這裡判斷waitStatus不為0就意味著下一個節點是阻塞狀態,然後就可以喚醒了
19        如果為0就沒有必要喚醒,因為下一個節點本身就是處於非阻塞狀態
20         */
21        if (h != null && h.waitStatus != 0)
22            unparkSuccessor(h);
23        return true;
24    }
25    return false;
26}
27
28  /**
29   * ReentrantLock:
30   * 第13行程式碼處:
31   */
32  protected final boolean tryRelease(int releases) {
33    //c = state - 1
34    int c = getState() - releases;
35    //如果當前執行緒不是上鎖時的執行緒,則丟擲異常
36    if (Thread.currentThread() != getExclusiveOwnerThread())
37        throw new IllegalMonitorStateException();
38    boolean free = false;
39    //如果減完1後的state是0的話,也就是沒有可重入鎖發生的情況,則可以將獨佔鎖擁有者設定為null
40    if (c == 0) {
41        free = true;
42        setExclusiveOwnerThread(null);
43    }
44    //設定state為減完1後的結果
45    setState(c);
46    return free;
47  }

4 公平鎖

ReentrantLock的公平鎖和非公平鎖實現上的區別寥寥無幾,只有lock方法和tryAcquire方法是不同的(包括unlock解鎖方法的實現都是一樣的),也就是FairSync類中覆寫的兩個方法。所以下面就來看一下這兩個方法的實現。

4.1 lock方法

 1  /**
 2   * ReentrantLock:
 3   */
 4  final void lock() {
 5    /*
 6    可以看到在公平鎖模式下,只是呼叫了acquire方法而已。而在非公平鎖模式下,會首先執行
 7    compareAndSetState,如果CAS失敗才呼叫acquire方法。這個意思也就是說:非公平鎖
 8    模式下的每個執行緒在加鎖時會首先嚐試加一下鎖,去賭一下此時是否釋放鎖了。如果釋放了,
 9    那麼此時的這個執行緒就能搶到鎖,相當於插隊了(這也就是“非公平”的含義)。如果沒搶到就
10    繼續去CLH佇列中排隊。而公平鎖模式下的每個執行緒加鎖時都只是會去乖乖排隊而已
11     */
12    acquire(1);
13  }

4.2 tryAcquire方法

 1  /**
 2   * ReentrantLock:
 3   * 可以看到公平鎖模式下的tryAcquire方法和非公平鎖模式下的nonfairTryAcquire方法的區別
 4   * 僅僅是多呼叫了一次hasQueuedPredecessors方法,其他都是一樣的。所以下面就來看一下該
 5   * 方法的實現
 6   */
 7  protected final boolean tryAcquire(int acquires) {
 8    final Thread current = Thread.currentThread();
 9    int c = getState();
10    if (c == 0) {
11        if (!hasQueuedPredecessors() &&
12                compareAndSetState(0, acquires)) {
13            setExclusiveOwnerThread(current);
14            return true;
15        }
16    } else if (current == getExclusiveOwnerThread()) {
17        int nextc = c + acquires;
18        if (nextc < 0)
19            throw new Error("Maximum lock count exceeded");
20        setState(nextc);
21        return true;
22    }
23    return false;
24  }

4.3 hasQueuedPredecessors方法

在上面tryAcquire方法中的第11行程式碼處會呼叫hasQueuedPredecessors方法,所以下面來看一下其實現:

 1  /**
 2   * ReentrantLock:
 3   * 本方法是用來判斷CLH佇列中是否已經有不是當前執行緒的其他節點,
 4   * 因為CLH佇列都是FIFO的,head.next節點一定是等待時間最久的,
 5   * 所以只需要跟它比較就行了。這裡也就是在找CLH佇列中是否有執行緒
 6   * 的等待獲取鎖的時間比當前執行緒的還要長。如果有的話當前執行緒就
 7   * 不會繼續後面的加鎖操作(這裡再次體現“公平”的含義),沒有
 8   * 才會嘗試加鎖
 9   */
10  public final boolean hasQueuedPredecessors() {
11    Node t = tail;
12    Node h = head;
13    Node s;
14    /*
15    <1>首先判斷head和tail是否不等,如果相等的話有兩種情況:head和tail都為null,或者是head和tail
16    都指向那個空節點(當最後僅剩下兩個節點的時候(一個空節點和一個真正等待的節點),此時再喚醒節點
17    的話,CLH佇列中此時就會僅剩一個空節點了)。不管屬於哪種,都代表著此時的CLH佇列中沒有在阻塞著的
18    節點了,那麼這個時候當前執行緒就可以嘗試加鎖了;
19       <2.1>如果此時CLH佇列中有節點的話,那麼就判斷一下head.next是否為空。我能想到的一種極端場景是:
20    假設此時CLH佇列中僅有一個空節點(head和tail都指向它),就在此刻一個新的節點需要進入CLH佇列裡,
21    它走到了addWaiter方法中,在執行完了compareAndSetTail後,但是還沒執行下面的“pred.next = node;”
22    之前,那麼當前執行緒獲取到的tail和head之間就僅有一個prev指標相連,而next指標此時還沒有進行連線
23    那麼此時獲取到的head.next就是null了,這種情況下當前執行緒也不會嘗試加鎖,而是去CLH佇列中排隊
24    (這種情況下雖然h.next是null,但是是有一個等待時間比當前執行緒還久的節點的,只不過它的指標還沒有
25    來得及連線上而已。所以當前節點會繼續去排隊,以此體現“公平”的含義);
26       <2.2>如果此時CLH佇列中有節點,並且不屬於上面第2.1條件中的特殊情況的話,還會去判斷head.next
27    是否是當前執行緒。這個時候出現的場景就是:當前執行緒會在CLH佇列中的head.next處,然後當前執行緒會再次在
28    本方法中進行判斷。那麼這是怎麼發生的呢?一種可能的情況是:當之前持有鎖的執行緒執行完畢釋放了之後,
29    這個時候的隊頭節點會被喚醒,從而走到acquireQueued方法中的tryAcquire方法處,然後再走到本方法中
30    這個時候的當前執行緒就是被喚醒的這個執行緒,所以s.thread != Thread.currentThread()這個條件不成立,
31    此時當前執行緒就可以嘗試加鎖了。如果head.next不是當前執行緒,也就是當前執行緒不是等待時間最久的那個執行緒
32    此時就不會去加鎖而是去排隊去了(再次體現“公平”的含義)
33     */
34    return h != t &&
35            ((s = h.next) == null || s.thread != Thread.currentThread());
36  }
下一篇將繼續分析AQS中共享模式的實現,敬請關注。原創文章,未得准許,請勿轉載,翻版必究要吐槽Doug Lea的請在下面排好隊

更多內容請關注微信公眾號:奇客時間

相關文章