在之前介紹AQS源碼的時候,還遺留了一個內部類ConditionObject沒有介紹,它也是並發中至關重要的類。
主要作用
Condition主要提供多個await方法以及signal、signalAll方法,對標的是Object的wait、notify、notifyAll方法,對應的作用也一樣。
ConditionObject是AQS的內部類並且實現了接口Condition,主要提供的功能也一樣。
創建方法
只有ReentrantLock提供了獲取AQS的Condition對象的方法。首先要new一個ReentrantLock對象,然後通過ReentrantLock的newCondition()方法獲取Condition對象,每次獲取都是創建一個新的Condition對象。
結構與主要方法
Condition(表示AQS中的ConditionObject,下同)只有兩個屬性firstWaiter、lastWaiter,類型是AQS的Node,也就是Condition也維護一個鍊表,firstWaiter、lastWaiter分別是鍊表的首位節點。
await方法源碼分析
Condition提供了很多個await方法,主要區別在於是否在如果等待一定時間後不是否繼續等待,這裡分析下await最基礎的無參方法,原始碼流程圖如下圖:
簡單分析下await方法:
首先創建一個Node(當前線程,狀態-2)節點並加到等待鍊表的尾部,這個Node是AQS中的Node,也會保存當前線程;
第二步是釋放鎖,因為線程在等待過程中其他線程也需要使用lock,所以這裡先釋放鎖,也就是修改AQS中state的值。
第三步是進入一個while循環,跳出循環判斷的條件是節點狀態不等於-2、並且要在同步隊列中(在AQS維護的鍊表中),如果沒在同步隊列中則會進入循環掛起線程,當再次被喚醒的時候會優先判斷線程是否中斷,如果線程中斷也會跳出循環,否則繼續循環判斷。
第四步是噹噹前線程跳出循環時,會重新設置鎖,設置成功再去根據情況整理鍊表,如果線程是中斷的,則根據情況拋出中斷異常,或者中斷。
從源碼中可以得出,在調用await方法之前一定要先獲取到鎖,否則會拋出異常。
signal方法:讓線程跳出循環
知道了await方法的源碼流程,就可以猜測signal方法的源碼了,先來分析下await方法如何才能執行完,先不考慮中斷線程的情況,要讓線程跳出while循環需要兩個條件:節點狀態不能是-2、讓節點在同步隊列中;記住這裡的同步隊列是AQS維護的同步隊列。
所以現在再來看signal方法的源碼就簡單了:
首先是拿到firstWaiter,如果為null則繼續往鍊表後面找;
第二步是把firstWaiter的狀態從等待狀態改為0,如果修改失敗則直接返回false,說明這個節點以及被修改成其他狀態了,繼續尋找下一個節點。
第三步把節點加到同步隊列的尾部;
第四步根據同步隊列的前置節點狀態判斷是否需要直接喚醒當前節點中的線程;
signal方法就是把Condition中維護的鍊表節點的頭部節點狀態設置為0並且加到AQS的同步隊列中。signalAll方法就是把鍊表的所有節點走一下signal方法的流程。
Condition與Object
分析了Condition方法但是並沒有體現它相比於Object的wait、notify、notifyAll的優勢,實際上這個問題在之前的文章設計阻塞隊列中有對比,這裡大概講一下,阻塞隊列有put與take方法,當隊列滿了put方法會阻塞,當隊列是空時take方法會阻塞。
用Object的wait、notifyAll方法來實現put、take方法的阻塞,在多線程情況下一個put方法獲取到鎖,那麼所有的包括put方法和take方法都會阻塞,當put方法執行完成後會喚醒take線程,但同時put線程也會被喚醒來搶執行權,但是如果此時隊列已滿,實際上所有的put線程都不應該會喚醒。
如果採用ReentrantLock來new出兩個Condition來實現,put方法如果發現隊列滿了則調用一個Condition的await方法來阻塞線程,另外一個來阻塞take方法,當put方法執行完後只用調用阻塞take方法中的Condition的signal,只用喚醒隊列中最前面一個就夠了。
總的來說可以利用一個ReentrantLock來實現對資源的訪問控制,利用多個Condition來更加精準的控制線程的阻塞與喚醒,相比利用一個Object來控制更加準確,可以使程序運行的更加高效。
總結
最後再梳理下Condition的await、signal方法,再使用await方法之前必須調用Condition對應的ReentrantLock的lock方法,也就是必須要獲取鎖。線程要跳出await方法就必須把節點放到AQS中的同步隊列中。而signal方法就是把節點放到同步隊列中。
再整理下整個流程:lock方法直到獲取鎖、調用await方法(生成一個Node放到Condition鍊表的末尾)、然後釋放鎖、進入while循環掛起線程、另外的線程獲取到鎖調用signal修改Condition鍊表的頭部節點狀態並放到AQS同步隊列中、當其他線程釋放鎖會去喚醒AQS同步隊列頭部線程、如果這個節點被喚醒就繼續執行await的while判斷會跳出循環、設置鎖狀態await方法執行完成、最後再調用unlock方法釋放鎖。
ReentrantLock用來維護同步隊列保證只有一個線程訪問資源、await方法把保存線程的節點加到Condition維護的鍊表中,signal方法把節點加到同步隊列中,通過這樣無論是await方法被喚醒還是其他地方ReentrantLock調用了lock都能保證資源只被最多一個線程訪問。
ReentrantLock與Condition在其他並發類中經常遇到,值得弄得他們的原理與優勢,便於理解其他並發類。
Java程式設計師日常學習筆記,如理解有誤歡迎各位交流討論!