1. 程式人生 > >ReentrantLock和condition原始碼淺析(一)

ReentrantLock和condition原始碼淺析(一)

 轉載請註明出處。。。。。

一、介紹

大家都知道,在java中如果要對一段程式碼做執行緒安全操作,都用到了鎖,當然鎖的實現很多,用的比較多的是sysnchronize和reentrantLock,前者是java裡的一個關鍵字,後者是一個java類。這兩者的大致區別,在這裡羅列下

相同點:

       1、都能保證了執行緒安全性

       2、都支援鎖的重入

不同點:

      1、synchronized適用於不是很激烈的情況,reentranLock適用於比較競爭激烈的情況

      2、Synchronized是jvm層面實現的鎖機制,而reentranLock是java程式碼層面實現的鎖機制。

      3、Reentranlock比synchronized多了鎖投票,定時鎖,中斷鎖等機制

      4、synchronized是隱式獲取鎖和釋放鎖,不需要程式碼手動獲取釋放,Reentranlock為顯示獲取鎖和釋放鎖,必須要手動程式碼獲取釋放

要了解reentranlock,那肯定先得會用它,下面通過一個例子來了解它的加鎖和釋放鎖過程

二、demo

 1
public class Demo { 2 3 private static int count = 0; 4 5 public static void main(String[] args) throws InterruptedException { 6 ExecutorService executorService = Executors.newFixedThreadPool(15); 7 for (int i = 0; i < 500; i++){ 8 executorService.execute(() -> {
9 add(); 10 }); 11 } 12 Thread.sleep(1000); 13 System.out.println(count); 14 } 15 16 private static int add(){ 17 return ++count; 18 } 19 }

 

 上述程式碼,安裝預期結果 那肯定是500,但是真的是500嗎?結果如下

結果很顯然,它是小於500的,把這段程式碼用鎖保證結果和預期結果一致。程式碼如下

 1 public class Demo {
 2 
 3     private static int count = 0;
 4 
 5     private static Lock lock = new ReentrantLock();
 6 
 7     public static void main(String[] args) throws InterruptedException {
 8         ExecutorService executorService = Executors.newFixedThreadPool(15);
 9         for (int i = 0; i < 500; i++){
10             executorService.execute(() -> {
11                 add();
12             });
13         }
14         Thread.sleep(1000);
15         System.out.println(count);
16     }
17 
18     private static int add(){
19         lock.lock();
20         try {
21             return ++count;
22         }finally {
23             lock.unlock();
24         }
25 
26     }
27 }

結果,和預期一致。

那它是怎麼保證執行緒安全性的呢。往下看

三、ReentrantLock分析

 先來了解這個類的大致結構

紅框圈中的三個類,其中Sync是一個抽象類,另外兩個是它的子類,Sync又繼承了AQS類,所以它也有鎖的操作可能性。

FairSync是一個公平鎖,NonFairSync是一個非公平鎖,它們雖然繼承了同一個類,但實現上有所不同,

1、非公平鎖獲取鎖的過程

進入lock方法

而sync 是ReentrantLock的一個欄位,它在該類的建構函式中初始化,它有兩個建構函式,sync預設為非公平鎖實現,

當sync呼叫了lock方法,也就是呼叫NonFairSync類的lock方法,繼續看下去,下圖為該類的結構

 

lock大致步驟為,先去試著改變state的值,如果改變成功,則state值就變為1了,返回true,失敗返回false,先來解釋下compareAndSetState方法的作用

它有兩個引數,第一個是期望值,第二個是要更新的值,如果記憶體中state值和期望值相等,則將記憶體值變為更新值,這是交換成功的標誌。如果不相等,那肯定是false。這個方法其實就是CAS,同時它也是執行緒安全的,具體實現,這裡不作討論。

這裡也是獲取鎖成功的標誌,當返回true,則將獲取鎖的執行緒置為當前執行緒,同時state值改變了,如果下一個執行緒進入,那麼該方法肯定是返回false。那麼獲取鎖失敗的執行緒就會進入acquire方法。這個方法其實就是AQS的方法,程式碼如下,可以看到它又呼叫了tryAcquire方法,而這個方法的實現就是上一個圖的nonFairTryAcquire方法,

 1 final boolean nonfairTryAcquire(int acquires) {
 2             // 獲取當前執行緒
 3             final Thread current = Thread.currentThread();
 4             int c = getState();
 5             // 如果狀態值不為0,則進一步去獲取鎖
 6             if (c == 0) {
 7                 if (compareAndSetState(0, acquires)) {
 8                     // 獲取鎖成功,將鎖置給當前執行緒
 9                     setExclusiveOwnerThread(current);
10                     return true;
11                 }
12             }// 如果相等,則表明為鎖的重入
13             else if (current == getExclusiveOwnerThread()) {
14                 int nextc = c + acquires;
15                 if (nextc < 0) // overflow
16                     throw new Error("Maximum lock count exceeded");
17                 setState(nextc);
18                 return true;
19             }
20            //  只有獲取鎖失敗才會返回false
21             return false;
22         }

 

 當上面返回false時,又會相繼執行addWaiter和acquireQueued方法,其中addWaiter方法主要是將獲取鎖失敗的執行緒包裝成一個Node節點,插入一個佇列中,注意頭結點不是該節點,而是new了一個新的node節點

,它的狀態值為0,然後返回該節點。

具體程式碼不做分析,下面看acquireQueued方法

 1  final boolean acquireQueued(final Node node, int arg) {
 2         boolean failed = true;
 3         try {
 4             boolean interrupted = false;
 5             // 類似while(true),作無限迴圈作用
 6             for (;;) {
 7                 // 獲取插入的node節點的前一個節點
 8                 final Node p = node.predecessor();
 9                 // 如果前繼節點為head節點並且獲取鎖成功,則跳出無限迴圈,執行相應業務程式碼
10                 if (p == head && tryAcquire(arg)) {
11                     setHead(node);// 頭結點被改變,改變同時其狀態也被改變了,節點執行緒也為空
12                     p.next = null; // help GC
13                     failed = false;
14                     return interrupted;
15                 }
16                 // 前繼節點不是頭結點或獲取鎖失敗
17                 if (shouldParkAfterFailedAcquire(p, node) &&
18                     parkAndCheckInterrupt())
19                     interrupted = true;
20             }
21         } finally {
22             // 防止程式碼執行過程中,執行緒突然被中斷,中斷則將該節點置為取消狀態
23             if (failed)
24                 cancelAcquire(node);
25         }
26     }

 

其中shouldParkAfterFailedAcquire方法做了這兩件事,

1、如果p節點前有狀態為cancel的節點,則將這些取消的節點放棄掉,簡單來說就是排除取消的節點

2、將p節點狀態置為signal狀態。等待下一次進入該方法可能會被掛起

方法parkAndCheckInterrupt,在shouldParkAfterFailedAcquire返回true的時候,執行緒會被掛起。、

以上就是獲取鎖的過程,步驟如下

1、獲取鎖成功,則將改變state值,並將鎖的擁有者置為當前執行緒

2、獲取鎖失敗,則進入同步佇列中,直到獲取鎖成功或當前執行緒被外因給中斷,獲取鎖的過程中,有的執行緒可能會被掛起。

2、非公平鎖釋放鎖的過程

為了不顯得過於囉嗦,下面只列出核心程式碼

上述程式碼只有獲取鎖的執行緒呼叫了unlock方法,才會去修改state值,當state值為0時其他執行緒又可以獲取鎖,看到這,或許有的小夥伴迷糊了,上面不是介紹說在獲取鎖的過程中,有的執行緒會被掛起,

那如果掛起的執行緒node節點前繼恰好是頭結點,那豈不是執行不了?,莫慌,往下看。當state值置為0時,該方法會返回true,之後會執行下面方法。

 重點方法在unparkSuccessor方法上,看if(h != null && h.waitStatus !=0) ,為什麼要加這個判斷呢,因為如果有多個執行緒在獲取鎖,無論是獲取失敗,還是獲取成功head節點的狀態值都被改變(setHead()和shouldParkAfterFailedAcquire()方法會去改變head節點狀態)。即不為0

如果為0,那麼就說明就沒有執行緒被掛起,自然就不用去釋放這些執行緒。加這個判斷,為了減少無用操作。重點來了,unparkSuccessor方法,程式碼如下

private void unparkSuccessor(Node node) {
        // 將node結點狀態置為0
        int ws = node.waitStatus;
        if (ws < 0)
            compareAndSetWaitStatus(node, ws, 0);

        /* 
         * 如果node結點下一個節點為null或被取消則進入下面的for迴圈
         *    下面的for迴圈從尾節點往前尋找沒有取消的節點 ,直至最靠近node節點,即node節點下一個狀態小於等於0的節點
         *  在這裡node節點就是頭結點,
         */
        Node s = node.next;
        if (s == null || s.waitStatus > 0) {
            s = null;
            for (Node t = tail; t != null && t != node; t = t.prev)
                if (t.waitStatus <= 0)
                    s = t;
        }
       // 找到了該節點,釋放該節點的執行緒
        if (s != null)
            LockSupport.unpark(s.thread);
    }    

 

或許看到這更迷糊了,它釋放鎖怎麼能確定釋放的就是那個被掛起的執行緒呢,這個呢,確實確定不了,但是如果釋放前繼節點為頭結點的執行緒,那麼在後續獲取鎖的過程中,該執行緒肯定能獲取到鎖(因為這段程式碼是前一個執行緒釋放鎖的操作程式碼,所以下一個執行緒肯定能獲取到鎖),至此又一輪迴圈。

在這裡,我對那個為啥從尾節點向前遍歷也不清楚,如果有清楚的小夥伴,還請評論下方留言,謝謝!

以上就是非公平鎖的釋放操作。

3、公平鎖的獲取鎖過程

該種鎖和非公平鎖的不同之處,就是這種鎖一定得按照順序來獲取或,不能前一個執行緒釋放了鎖 ,然後誰搶到了就算誰的。

先來看下這種獲取鎖的程式碼

 1 protected final boolean tryAcquire(int acquires) {
 2             final Thread current = Thread.currentThread();
 3             int c = getState();
 4             if (c == 0) {
 5                 if (!hasQueuedPredecessors() &&
 6                     compareAndSetState(0, acquires)) {
 7                     setExclusiveOwnerThread(current);
 8                     return true;
 9                 }
10             }
11             else if (current == getExclusiveOwnerThread()) {
12                 int nextc = c + acquires;
13                 if (nextc < 0)
14                     throw new Error("Maximum lock count exceeded");
15                 setState(nextc);
16                 return true;
17             }
18             return false;
19         }

 

和非公平鎖的不同點是在前者執行緒釋放鎖後(即state值為0),非公平鎖是誰搶到鎖,鎖就是誰的,但是公平鎖不一樣,獲取鎖的執行緒會先去判斷同步佇列中有沒有其他執行緒,如果沒有,再去試著改變state值,如果改變成功則獲取鎖成功,它不允許沒進入同步佇列中的執行緒(此時同步佇列中已有等待的執行緒,如果沒有,那就是直接搶)搶佔鎖。下面看下hasQueuedPrdecessor(),程式碼如下

 1 public final boolean hasQueuedPredecessors() {
 2         // The correctness of this depends on head being initialized
 3         // before tail and on head.next being accurate if the current
 4         // thread is first in queue.
 5         Node t = tail; // Read fields in reverse initialization order
 6         Node h = head;
 7         Node s;
 8         return h != t &&
 9             ((s = h.next) == null || s.thread != Thread.currentThread());
10     }

 

程式碼不復雜,就是判斷同步佇列中有沒有等待的執行緒,且等待的執行緒不是當前執行緒,有則返回true,沒有則返回false。

至於公平鎖的釋放操作,和非公平鎖一致。這裡不過多敘述。

獲取公平鎖操作

1、先判斷同步佇列中有沒有等待的執行緒。

2、有則放棄鎖的爭奪,進入同步佇列排好隊,沒有則搶佔鎖

 ----------------------------------------------------------------------------------------------------華麗的分界線---------------------------------------------------------------------------------------------------------------------------------

 本來想繼續寫condition,但好像篇幅有點囉嗦,就放在下一篇。

以上就是我的個人見解,如果不足或錯誤之處,還請指教,謝謝!