主頁 > 後端開發 > AQS原始碼深入分析之獨占模式-ReentrantLock鎖特性詳解

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

2020-11-03 05:24:59 後端開發

本文基于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的請在下面排好隊

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

轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/200486.html

標籤:Java

上一篇:分享一個小清新的論壇原始碼

下一篇:30道 有趣的 的 JVM 面試題

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • 【C++】Microsoft C++、C 和匯編程式檔案

    ......

    uj5u.com 2020-09-10 00:57:23 more
  • 例外宣告

    相比于斷言適用于排除邏輯上不可能存在的狀態,例外通常是用于邏輯上可能發生的錯誤。 例外宣告 Item 1:當函式不可能拋出例外或不能接受拋出例外時,使用noexcept 理由 如果不打算拋出例外的話,程式就會認為無法處理這種錯誤,并且應當盡早終止,如此可以有效地阻止例外的傳播與擴散。 示例 //不可 ......

    uj5u.com 2020-09-10 00:57:27 more
  • Codeforces 1400E Clear the Multiset(貪心 + 分治)

    鏈接:https://codeforces.com/problemset/problem/1400/E 來源:Codeforces 思路:給你一個陣列,現在你可以進行兩種操作,操作1:將一段沒有 0 的區間進行減一的操作,操作2:將 i 位置上的元素歸零。最終問:將這個陣列的全部元素歸零后操作的最少 ......

    uj5u.com 2020-09-10 00:57:30 more
  • UVA11610 【Reverse Prime】

    本人看到此題沒有翻譯,就附帶了一個自己的翻譯版本 思考 這一題,它的第一個要求是找出所有 $7$ 位反向質數及其質因數的個數。 我們應該需要質數篩篩選1~$10^{7}$的所有數,這里就不慢慢介紹了。但是,重讀題,我們突然發現反向質數都是 $7$ 位,而將它反過來后的數字卻是 $6$ 位數,這就說明 ......

    uj5u.com 2020-09-10 00:57:36 more
  • 統計區間素數數量

    1 #pragma GCC optimize(2) 2 #include <bits/stdc++.h> 3 using namespace std; 4 bool isprime[1000000010]; 5 vector<int> prime; 6 inline int getlist(int ......

    uj5u.com 2020-09-10 00:57:47 more
  • C/C++編程筆記:C++中的 const 變數詳解,教你正確認識const用法

    1、C中的const 1、區域const變數存放在堆疊區中,會分配記憶體(也就是說可以通過地址間接修改變數的值)。測驗代碼如下: 運行結果: 2、全域const變數存放在只讀資料段(不能通過地址修改,會發生寫入錯誤), 默認為外部聯編,可以給其他源檔案使用(需要用extern關鍵字修飾) 運行結果: ......

    uj5u.com 2020-09-10 00:58:04 more
  • 【C++犯錯記錄】VS2019 MFC添加資源不懂如何修改資源宏ID

    1. 首先在資源視圖中,添加資源 2. 點擊新添加的資源,復制自動生成的ID 3. 在解決方案資源管理器中找到Resource.h檔案,編輯,使用整個專案搜索和替換的方式快速替換 宏宣告 4. Ctrl+Shift+F 全域搜索,點擊查找全部,然后逐個替換 5. 為什么使用搜索替換而不使用屬性視窗直 ......

    uj5u.com 2020-09-10 00:59:11 more
  • 【C++犯錯記錄】VS2019 MFC不懂的批量添加資源

    1. 打開資源頭檔案Resource.h,在其中預先定義好宏 ID(不清楚其實ID值應該設定多少,可以先新建一個相同的資源項,再在這個資源的ID值的基礎上遞增即可) 2. 在資源視圖中選中專案資源,按F7編輯資源檔案,按 ID 型別 相對路徑的形式添加 資源。(別忘了先把檔案拷貝到專案中的res檔案 ......

    uj5u.com 2020-09-10 01:00:19 more
  • C/C++編程筆記:關于C++的參考型別,專供新手入門使用

    今天要講的是C++中我最喜歡的一個用法——參考,也叫別名。 參考就是給一個變數名取一個變數名,方便我們間接地使用這個變數。我們可以給一個變數創建N個參考,這N + 1個變數共享了同一塊記憶體區域。(參考型別的變數會占用記憶體空間,占用的記憶體空間的大小和指標型別的大小是相同的。雖然參考是一個物件的別名,但 ......

    uj5u.com 2020-09-10 01:00:22 more
  • 【C/C++編程筆記】從頭開始學習C ++:初學者完整指南

    眾所周知,C ++的學習曲線陡峭,但是花時間學習這種語言將為您的職業帶來奇跡,并使您與其他開發人員區分開。您會更輕松地學習新語言,形成真正的解決問題的技能,并在編程的基礎上打下堅實的基礎。 C ++將幫助您養成良好的編程習慣(即清晰一致的編碼風格,在撰寫代碼時注釋代碼,并限制類內部的可見性),并且由 ......

    uj5u.com 2020-09-10 01:00:41 more
最新发布
  • Rust中的智能指標:Box<T> Rc<T> Arc<T> Cell<T> RefCell<T> Weak

    Rust中的智能指標是什么 智能指標(smart pointers)是一類資料結構,是擁有資料所有權和額外功能的指標。是指標的進一步發展 指標(pointer)是一個包含記憶體地址的變數的通用概念。這個地址參考,或 ” 指向”(points at)一些其 他資料 。參考以 & 符號為標志并借用了他們所 ......

    uj5u.com 2023-04-20 07:24:10 more
  • Java的值傳遞和參考傳遞

    值傳遞不會改變本身,參考傳遞(如果傳遞的值需要實體化到堆里)如果發生修改了會改變本身。 1.基本資料型別都是值傳遞 package com.example.basic; public class Test { public static void main(String[] args) { int ......

    uj5u.com 2023-04-20 07:24:04 more
  • [2]SpinalHDL教程——Scala簡單入門

    第一個 Scala 程式 shell里面輸入 $ scala scala> 1 + 1 res0: Int = 2 scala> println("Hello World!") Hello World! 檔案形式 object HelloWorld { /* 這是我的第一個 Scala 程式 * 以 ......

    uj5u.com 2023-04-20 07:23:58 more
  • 理解函式指標和回呼函式

    理解 函式指標 指向函式的指標。比如: 理解函式指標的偽代碼 void (*p)(int type, char *data); // 定義一個函式指標p void func(int type, char *data); // 宣告一個函式func p = func; // 將指標p指向函式func ......

    uj5u.com 2023-04-20 07:23:52 more
  • Django筆記二十五之資料庫函式之日期函式

    本文首發于公眾號:Hunter后端 原文鏈接:Django筆記二十五之資料庫函式之日期函式 日期函式主要介紹兩個大類,Extract() 和 Trunc() Extract() 函式作用是提取日期,比如我們可以提取一個日期欄位的年份,月份,日等資料 Trunc() 的作用則是截取,比如 2022-0 ......

    uj5u.com 2023-04-20 07:23:45 more
  • 一天吃透JVM面試八股文

    什么是JVM? JVM,全稱Java Virtual Machine(Java虛擬機),是通過在實際的計算機上仿真模擬各種計算機功能來實作的。由一套位元組碼指令集、一組暫存器、一個堆疊、一個垃圾回收堆和一個存盤方法域等組成。JVM屏蔽了與作業系統平臺相關的資訊,使得Java程式只需要生成在Java虛擬機 ......

    uj5u.com 2023-04-20 07:23:31 more
  • 使用Java接入小程式訂閱訊息!

    更新完微信服務號的模板訊息之后,我又趕緊把微信小程式的訂閱訊息給實作了!之前我一直以為微信小程式也是要企業才能申請,沒想到小程式個人就能申請。 訊息推送平臺🔥推送下發【郵件】【短信】【微信服務號】【微信小程式】【企業微信】【釘釘】等訊息型別。 https://gitee.com/zhongfuch ......

    uj5u.com 2023-04-20 07:22:59 more
  • java -- 緩沖流、轉換流、序列化流

    緩沖流 緩沖流, 也叫高效流, 按照資料型別分類: 位元組緩沖流:BufferedInputStream,BufferedOutputStream 字符緩沖流:BufferedReader,BufferedWriter 緩沖流的基本原理,是在創建流物件時,會創建一個內置的默認大小的緩沖區陣列,通過緩沖 ......

    uj5u.com 2023-04-20 07:22:49 more
  • Java-SpringBoot-Range請求頭設定實作視頻分段傳輸

    老實說,人太懶了,現在基本都不喜歡寫筆記了,但是網上有關Range請求頭的文章都太水了 下面是抄的一段StackOverflow的代碼...自己大修改過的,寫的注釋挺全的,應該直接看得懂,就不解釋了 寫的不好...只是希望能給視頻網站開發的新手一點點幫助吧. 業務場景:視頻分段傳輸、視頻多段傳輸(理 ......

    uj5u.com 2023-04-20 07:22:42 more
  • Windows 10開發教程_編程入門自學教程_菜鳥教程-免費教程分享

    教程簡介 Windows 10開發入門教程 - 從簡單的步驟了解Windows 10開發,從基本到高級概念,包括簡介,UWP,第一個應用程式,商店,XAML控制元件,資料系結,XAML性能,自適應設計,自適應UI,自適應代碼,檔案管理,SQLite資料庫,應用程式到應用程式通信,應用程式本地化,應用程式 ......

    uj5u.com 2023-04-20 07:22:35 more