Java併發系列[3]----AbstractQueuedSynchronizer源碼分析之共享模式

經過上一篇的分析,咱們知道了獨佔模式獲取鎖有三種方式,分別是不響應線程中斷獲取,響應線程中斷獲取,設置超時時間獲取。在共享模式下獲取鎖的方式也是這三種,並且基本上都是大同小異,咱們搞清楚了一種就能很快的理解其餘的方式。雖說AbstractQueuedSynchronizer源碼有一千多行,可是重複的也比較多,因此讀者不要剛開始的時候被嚇到,只要耐着性子去看慢慢的天然可以漸漸領悟。就我我的經驗來講,閱讀AbstractQueuedSynchronizer源碼有幾個比較關鍵的地方須要弄明白,分別是獨佔模式和共享模式的區別,結點的等待狀態,以及對條件隊列的理解。理解了這些要點那麼後續源碼的閱讀將會輕鬆不少。固然這些在個人《Java併發系列[1]----AbstractQueuedSynchronizer源碼分析之概要分析》這篇文章裏都有詳細的介紹,讀者能夠先去查閱。本篇對於共享模式的分析也是分爲三種獲取鎖的方式和一種釋放鎖的方式。html

1. 不響應線程中斷的獲取node

 1 //以不可中斷模式獲取鎖(共享模式)
 2 public final void acquireShared(int arg) {
 3     //1.嘗試去獲取鎖
 4     if (tryAcquireShared(arg) < 0) {
 5         //2.若是獲取失敗就進入這個方法
 6         doAcquireShared(arg);
 7     }
 8 }
 9 
10 //嘗試去獲取鎖(共享模式)
11 //負數:表示獲取失敗
12 //零值:表示當前結點獲取成功, 可是後繼結點不能再獲取了
13 //正數:表示當前結點獲取成功, 而且後繼結點一樣能夠獲取成功
14 protected int tryAcquireShared(int arg) {
15     throw new UnsupportedOperationException();
16 }

調用acquireShared方法是不響應線程中斷獲取鎖的方式。在該方法中,首先調用tryAcquireShared去嘗試獲取鎖,tryAcquireShared方法返回一個獲取鎖的狀態,這裏AQS規定了返回狀態如果負數表明當前結點獲取鎖失敗,如果0表明當前結點獲取鎖成功,但後繼結點不能再獲取了,如果正數則表明當前結點獲取鎖成功,而且這個鎖後續結點也一樣能夠獲取成功。子類在實現tryAcquireShared方法獲取鎖的邏輯時,返回值須要遵照這個約定。若是調用tryAcquireShared的返回值小於0,就表明此次嘗試獲取鎖失敗了,接下來就調用doAcquireShared方法將當前線程添加進同步隊列。咱們看到doAcquireShared方法。併發

 1 //在同步隊列中獲取(共享模式)
 2 private void doAcquireShared(int arg) {
 3     //添加到同步隊列中
 4     final Node node = addWaiter(Node.SHARED);
 5     boolean failed = true;
 6     try {
 7         boolean interrupted = false;
 8         for (;;) {
 9             //獲取當前結點的前繼結點
10             final Node p = node.predecessor();
11             //若是前繼結點爲head結點就再次嘗試去獲取鎖
12             if (p == head) {
13                 //再次嘗試去獲取鎖並返回獲取狀態
14                 //r < 0, 表示獲取失敗
15                 //r = 0, 表示當前結點獲取成功, 可是後繼結點不能再獲取了
16                 //r > 0, 表示當前結點獲取成功, 而且後繼結點一樣能夠獲取成功
17                 int r = tryAcquireShared(arg);
18                 if (r >= 0) {
19                     //到這裏說明當前結點已經獲取鎖成功了, 此時它會將鎖的狀態信息傳播給後繼結點
20                     setHeadAndPropagate(node, r);
21                     p.next = null;
22                     //若是在線程阻塞期間收到中斷請求, 就在這一步響應該請求
23                     if (interrupted) {
24                         selfInterrupt();
25                     }
26                     failed = false;
27                     return;
28                 }
29             }
30             //每次獲取鎖失敗後都會判斷是否能夠將線程掛起, 若是能夠的話就會在parkAndCheckInterrupt方法裏將線程掛起
31             if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) {
32                 interrupted = true;
33             }
34         }
35     } finally {
36         if (failed) {
37             cancelAcquire(node);
38         }
39     }
40 }

進入doAcquireShared方法首先是調用addWaiter方法將當前線程包裝成結點放到同步隊列尾部。這個添加結點的過程咱們在講獨佔模式時講過,這裏就再也不講了。結點進入同步隊列後,若是它發如今它前面的結點就是head結點,由於head結點的線程已經獲取鎖進入房間裏面了,那麼下一個獲取鎖的結點就輪到本身了,因此當前結點先不會將本身掛起,而是再一次去嘗試獲取鎖,若是前面那人恰好釋放鎖離開了,那麼當前結點就能成功得到鎖,若是前面那人尚未釋放鎖,那麼就會調用shouldParkAfterFailedAcquire方法,在這個方法裏面會將head結點的狀態改成SIGNAL,只有保證前面結點的狀態爲SIGNAL,當前結點才能放心的將本身掛起,全部線程都會在parkAndCheckInterrupt方法裏面被掛起。若是當前結點恰巧成功的獲取了鎖,那麼接下來就會調用setHeadAndPropagate方法將本身設置爲head結點,而且喚醒後面一樣是共享模式的結點。下面咱們看下setHeadAndPropagate方法具體的操做。源碼分析

 1 //設置head結點並傳播鎖的狀態(共享模式)
 2 private void setHeadAndPropagate(Node node, int propagate) {
 3     Node h = head;
 4     //將給定結點設置爲head結點
 5     setHead(node);
 6     //若是propagate大於0代表鎖能夠獲取了
 7     if (propagate > 0 || h == null || h.waitStatus < 0) {
 8         //獲取給定結點的後繼結點
 9         Node s = node.next;
10         //若是給定結點的後繼結點爲空, 或者它的狀態是共享狀態
11         if (s == null || s.isShared()) {
12             //喚醒後繼結點
13             doReleaseShared();
14         }
15     }
16 }
17 
18 //釋放鎖的操做(共享模式)
19 private void doReleaseShared() {
20     for (;;) {
21         //獲取同步隊列的head結點
22         Node h = head;
23         if (h != null && h != tail) {
24             //獲取head結點的等待狀態
25             int ws = h.waitStatus;
26             //若是head結點的狀態爲SIGNAL, 代表後面有人在排隊
27             if (ws == Node.SIGNAL) {
28                 //先把head結點的等待狀態更新爲0
29                 if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0)) {
30                     continue;
31                 }
32                 //再去喚醒後繼結點
33                 unparkSuccessor(h);
34              //若是head結點的狀態爲0, 代表此時後面沒人在排隊, 就只是將head狀態修改成PROPAGATE
35             }else if (ws == 0 && !compareAndSetWaitStatus(h, 0, Node.PROPAGATE)) {
36                 continue;
37             }
38         }
39         //只有保證期間head結點沒被修改過才能跳出循環
40         if (h == head) {
41             break;
42         }
43     }
44 }

調用setHeadAndPropagate方法首先將本身設置成head結點,而後再根據傳入的tryAcquireShared方法的返回值來決定是否要去喚醒後繼結點。前面已經講到當返回值大於0就代表當前結點成功獲取了鎖,而且後面的結點也能夠成功獲取鎖。這時當前結點就須要去喚醒後面一樣是共享模式的結點,注意,每次喚醒僅僅只是喚醒後一個結點,若是後一個結點不是共享模式的話,當前結點就直接進入房間而不會再去喚醒更後面的結點了。共享模式下喚醒後繼結點的操做是在doReleaseShared方法進行的,共享模式和獨佔模式的喚醒操做基本也是相同的,都是去找到本身座位上的牌子(等待狀態),若是牌子上爲SIGNAL代表後面有人須要讓它幫忙喚醒,若是牌子上爲0則代表隊列此時並無人在排隊。在獨佔模式下是若是發現沒人在排隊就直接離開隊列了,而在共享模式下若是發現隊列後面沒人在排隊,當前結點在離開前仍然會留個小紙條(將等待狀態設置爲PROPAGATE)告訴後來的人這個鎖的可獲取狀態。那麼後面來的人在嘗試獲取鎖的時候能夠根據這個狀態來判斷是否直接獲取鎖。ui

2. 響應線程中斷的獲取this

 1 //以可中斷模式獲取鎖(共享模式)
 2 public final void acquireSharedInterruptibly(int arg) throws InterruptedException {
 3     //首先判斷線程是否中斷, 若是是則拋出異常
 4     if (Thread.interrupted()) {
 5         throw new InterruptedException();
 6     }
 7     //1.嘗試去獲取鎖
 8     if (tryAcquireShared(arg) < 0) {
 9         //2. 若是獲取失敗則進人該方法
10         doAcquireSharedInterruptibly(arg);
11     }
12 }
13 
14 //以可中斷模式獲取(共享模式)
15 private void doAcquireSharedInterruptibly(int arg) throws InterruptedException {
16     //將當前結點插入同步隊列尾部
17     final Node node = addWaiter(Node.SHARED);
18     boolean failed = true;
19     try {
20         for (;;) {
21             //獲取當前結點的前繼結點
22             final Node p = node.predecessor();
23             if (p == head) {
24                 int r = tryAcquireShared(arg);
25                 if (r >= 0) {
26                     setHeadAndPropagate(node, r);
27                     p.next = null;
28                     failed = false;
29                     return;
30                 }
31             }
32             if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) {
33                 //若是線程在阻塞過程當中收到過中斷請求, 那麼就會立馬在這裏拋出異常
34                 throw new InterruptedException();
35             }
36         }
37     } finally {
38         if (failed) {
39             cancelAcquire(node);
40         }
41     }
42 }

響應線程中斷獲取鎖的方式和不響應線程中斷獲取鎖的方式在流程上基本是相同的,惟一的區別就是在哪裏響應線程的中斷請求。在不響應線程中斷獲取鎖時,線程從parkAndCheckInterrupt方法中被喚醒,喚醒後就立馬返回是否收到中斷請求,即便是收到了中斷請求也會繼續自旋直到獲取鎖後才響應中斷請求將本身給掛起。而響應線程中斷獲取鎖會才線程被喚醒後立馬響應中斷請求,若是在阻塞過程當中收到了線程中斷就會立馬拋出InterruptedException異常。spa

3. 設置超時時間的獲取線程

 1 //以限定超時時間獲取鎖(共享模式)
 2 public final boolean tryAcquireSharedNanos(int arg, long nanosTimeout) throws InterruptedException {
 3     if (Thread.interrupted()) {
 4         throw new InterruptedException();
 5     }
 6     //1.調用tryAcquireShared嘗試去獲取鎖
 7     //2.若是獲取失敗就調用doAcquireSharedNanos
 8     return tryAcquireShared(arg) >= 0 || doAcquireSharedNanos(arg, nanosTimeout);
 9 }
10 
11 //以限定超時時間獲取鎖(共享模式)
12 private boolean doAcquireSharedNanos(int arg, long nanosTimeout) throws InterruptedException {
13     long lastTime = System.nanoTime();
14     final Node node = addWaiter(Node.SHARED);
15     boolean failed = true;
16     try {
17         for (;;) {
18             //獲取當前結點的前繼結點
19             final Node p = node.predecessor();
20             if (p == head) {
21                 int r = tryAcquireShared(arg);
22                 if (r >= 0) {
23                     setHeadAndPropagate(node, r);
24                     p.next = null;
25                     failed = false;
26                     return true;
27                 }
28             }
29             //若是超時時間用完了就結束獲取, 並返回失敗信息
30             if (nanosTimeout <= 0) {
31                 return false;
32             }
33             //1.檢查是否知足將線程掛起要求(保證前繼結點狀態爲SIGNAL)
34             //2.檢查超時時間是否大於自旋時間
35             if (shouldParkAfterFailedAcquire(p, node) && nanosTimeout > spinForTimeoutThreshold) {
36                 //若知足上面兩個條件就將當前線程掛起一段時間
37                 LockSupport.parkNanos(this, nanosTimeout);
38             }
39             long now = System.nanoTime();
40             //超時時間每次減去獲取鎖的時間
41             nanosTimeout -= now - lastTime;
42             lastTime = now;
43             //若是在阻塞時收到中斷請求就立馬拋出異常
44             if (Thread.interrupted()) {
45                 throw new InterruptedException();
46             }
47         }
48     } finally {
49         if (failed) {
50             cancelAcquire(node);
51         }
52     }
53 }

若是看懂了上面兩種獲取方式,再來看設置超時時間的獲取方式就會很輕鬆,基本流程都是同樣的,主要是理解超時的機制是怎樣的。若是第一次獲取鎖失敗會調用doAcquireSharedNanos方法並傳入超時時間,進入方法後會根據狀況再次去獲取鎖,若是再次獲取失敗就要考慮將線程掛起了。這時會判斷超時時間是否大於自旋時間,若是是的話就會將線程掛起一段時間,不然就繼續嘗試獲取,每次獲取鎖以後都會將超時時間減去獲取鎖的時間,一直這樣循環直到超時時間用盡,若是尚未獲取到鎖的話就會結束獲取並返回獲取失敗標識。在整個期間線程是響應線程中斷的。code

4. 共享模式下結點的出隊操做htm

 1 //釋放鎖的操做(共享模式)
 2 public final boolean releaseShared(int arg) {
 3     //1.嘗試去釋放鎖
 4     if (tryReleaseShared(arg)) {
 5         //2.若是釋放成功就喚醒其餘線程
 6         doReleaseShared();
 7         return true;
 8     }
 9     return false;
10 }
11 
12 //嘗試去釋放鎖(共享模式)
13 protected boolean tryReleaseShared(int arg) {
14     throw new UnsupportedOperationException();
15 }
16 
17 //釋放鎖的操做(共享模式)
18 private void doReleaseShared() {
19     for (;;) {
20         //獲取同步隊列的head結點
21         Node h = head;
22         if (h != null && h != tail) {
23             //獲取head結點的等待狀態
24             int ws = h.waitStatus;
25             //若是head結點的狀態爲SIGNAL, 代表後面有人在排隊
26             if (ws == Node.SIGNAL) {
27                 //先把head結點的等待狀態更新爲0
28                 if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0)) {
29                     continue;
30                 }
31                 //再去喚醒後繼結點
32                 unparkSuccessor(h);
33              //若是head結點的狀態爲0, 代表此時後面沒人在排隊, 就只是將head狀態修改成PROPAGATE
34             }else if (ws == 0 && !compareAndSetWaitStatus(h, 0, Node.PROPAGATE)) {
35                 continue;
36             }
37         }
38         //只有保證期間head結點沒被修改過才能跳出循環
39         if (h == head) {
40             break;
41         }
42     }
43 }

線程在房間辦完事以後就會調用releaseShared方法釋放鎖,首先調用tryReleaseShared方法嘗試釋放鎖,該方法的判斷邏輯由子類實現。若是釋放成功就調用doReleaseShared方法去喚醒後繼結點。走出房間後它會找到原先的座位(head結點),看看座位上是否有人留了小紙條(狀態爲SIGNAL),若是有就去喚醒後繼結點。若是沒有(狀態爲0)就表明隊列沒人在排隊,那麼在離開以前它還要作最後一件事情,就是在本身座位上留下小紙條(狀態設置爲PROPAGATE),告訴後面的人鎖的獲取狀態,整個釋放鎖的過程和獨佔模式惟一的區別就是在這最後一步操做。

注:以上所有分析基於JDK1.7,不一樣版本間會有差別,讀者須要注意

相關文章
相關標籤/搜索