Java併發體系-第四階段-AQS原始碼解讀-[1]-【萬字長文系列】
1、文章可能會優先更新在Github,個人部落格,公眾號【Github有】。其它平臺會晚一段時間。個人部落格備用地址
2、如果Github很卡,可以在Gitee瀏覽,或者Gitee線上閱讀,個人部落格。Gitee線上閱讀和個人部落格載入速度比較快。
3、轉載須知:轉載請註明GitHub出處,讓我們一起維護一個良好的技術創作環境!
4、如果你要提交 issue 或者 pr 的話建議到 Github 提交。
5、筆者會陸續更新,如果對你有所幫助,不妨Github點個Star~。你的Star是我創作的動力。
關於Java併發系列的書籍筆者看過4本,Java併發的教程視訊看了4個機構的視訊。個人認為筆者的併發系列文章可能是市面上關於Java併發最詳細,深度最深的文章。如果有更好的文章,讀者可以在下方評論,為開源社群做點貢獻,也為其他人提供幫助。
可重入鎖
/**
* @Author: youthlql-呂
* @Date: 2020/10/22 21:12
* <p>
* 可重入鎖:
* 1、可重複可遞迴呼叫的鎖,在外層使用鎖之後,在內層仍然可以使用,並且不發生死鎖,這樣的鎖就叫做可重入鎖。
* 2、是指在同一個執行緒在外層方法獲取鎖的時候,再進入該執行緒的內層方法會自動獲取鎖(前提,鎖物件得是同一個物件),
* 不會因為之前已經獲取過還沒釋放而阻塞
*/
public class ReEnterLockDemo {
static Object objectLockA = new Object();
public static void m1(){
new Thread(() -> {
synchronized (objectLockA){
System.out.println(Thread.currentThread().getName()+"\t"+"------外層呼叫");
synchronized (objectLockA){
System.out.println(Thread.currentThread().getName()+"\t"+"------中層呼叫");
synchronized (objectLockA)
{
System.out.println(Thread.currentThread().getName()+"\t"+"------內層呼叫");
}
}
}
},"t1").start();
}
public static void main(String[] args) {
m1();
}
}
public class ReEnterLockDemo {
public synchronized void m1(){
System.out.println("=====外層");
m2();
}
public synchronized void m2() {
System.out.println("=====中層");
m3();
}
public synchronized void m3(){
System.out.println("=====內層");
}
public static void main(String[] args) {
new ReEnterLockDemo().m1();
}
}
LockSupport
是什麼?
官方說明:https://www.apiref.com/java11-zh/java.base/java/util/concurrent/locks/LockSupport.html
LockSupport中的park()和unpark()的作用分別是阻塞執行緒和解除阻塞執行緒,相當於執行緒等待和喚醒機制的加強版。
3種讓執行緒等待和喚醒的方法
- 方式1: 使用Object中的wait()方法讓執行緒等待, 使用Object中的notify()方法喚醒執行緒
- 方式2: 使用JUC包中Condition的await()方法讓執行緒等待,使用signal()方法喚醒執行緒
- 方式3: LockSupport類可以阻塞當前執行緒以及喚醒指定被阻塞的執行緒
Object類提供的等待喚醒機制的缺點
正常情況下
public class LockSupportDemo1 {
static Object objectLock = new Object();
public static void main(String[] args) {
new Thread(() -> {
synchronized (objectLock){
System.out.println(Thread.currentThread().getName()+"\t"+"------come in");
try {
objectLock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"\t"+"------被喚醒");
}
},"A").start();
new Thread(() -> {
synchronized (objectLock)
{
objectLock.notify();
System.out.println(Thread.currentThread().getName()+"\t"+"------通知");
}
},"B").start();
}
}
結果:
A ------come in
B ------通知
A ------被喚醒
Process finished with exit code 0
異常情況1
去掉同步程式碼塊
public class LockSupportDemo1 {
static Object objectLock = new Object();
public static void main(String[] args) {
new Thread(() -> {
// synchronized (objectLock){
System.out.println(Thread.currentThread().getName()+"\t"+"------come in");
try {
objectLock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"\t"+"------被喚醒");
// }
},"A").start();
new Thread(() -> {
// synchronized (objectLock){
objectLock.notify();
System.out.println(Thread.currentThread().getName()+"\t"+"------通知");
// }
},"B").start();
}
}
結果:
A ------come in
Exception in thread "A" Exception in thread "B" java.lang.IllegalMonitorStateException
at java.lang.Object.wait(Native Method)
at java.lang.Object.wait(Object.java:502)
at com.youth.guiguthirdquarter.AQS.LockSupportDemo1.lambda$main$0(LockSupportDemo1.java:16)
at java.lang.Thread.run(Thread.java:748)
java.lang.IllegalMonitorStateException
at java.lang.Object.notify(Native Method)
at com.youth.guiguthirdquarter.AQS.LockSupportDemo1.lambda$main$1(LockSupportDemo1.java:26)
at java.lang.Thread.run(Thread.java:748)
Process finished with exit code 0
報錯了。
異常情況2
先喚醒,再等待。
public class LockSupportDemo1 {
static Object objectLock = new Object();
public static void main(String[] args) {
new Thread(() -> {
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (objectLock){
System.out.println(Thread.currentThread().getName()+"\t"+"------come in");
try {
objectLock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"\t"+"------被喚醒");
}
},"A").start();
new Thread(() -> {
synchronized (objectLock)
{
objectLock.notify();
System.out.println(Thread.currentThread().getName()+"\t"+"------通知");
}
},"B").start();
}
}
結果:
B ------通知
A ------come in
Process finished with exit code -1
死迴圈,A無法被喚醒了。
這兩點我們之前也說過,Object類提供的wait和notify
1、只能在synchronized同步程式碼塊裡使用
2、只能先等待(wait),再喚醒(notify)。順序一旦錯了,那個等待執行緒就無法被喚醒了。
Condion類提供的等待喚醒機制的缺點
缺點和Object類裡的wait,notify一樣。
1、只能在lock同步程式碼塊裡使用,不然就報錯
2、只能先等待(await),再喚醒(signal)。順序一旦錯了,那個等待執行緒就無法被喚醒了。
但相對於wait,notify改進的一點是,可以繫結lock進行定向喚醒。
LockSupport的優點
有的時候我不需要進入同步程式碼塊,我只是需要讓執行緒阻塞,這個時候LockSupport就發揮作用了。並且還解決了之前的第二個問題,也就是等待必須在喚醒的前面。
static void park() //除非許可證可用,否則禁用當前執行緒以進行執行緒排程。
static void unpark(Thread thread) //如果給定執行緒尚不可用,則為其提供許可。
-
LockSupport是用來建立鎖和其他同步類的基本執行緒阻塞原語。
-
LockSupport類使用了一種名為Permit(許可)的概念來做到阻塞和喚醒執行緒的功能,每個執行緒都有一個許可(permit),
permit只有兩個值1和零,預設是零。可以把許可看成是一種(0,1)訊號量(Semaphore),但與Semaphore不同的是,許可的累加上限是1。
public static void park() {
UNSAFE.park(false, 0L);
}
LockSupport底層還是UNSAFE(前面講過)。
-
permit預設是0,所以一開始呼叫park()方法,當前執行緒就會阻塞,直到別的執行緒將當前執行緒的permit設定為1時,park方法會被喚醒,然後會將permit再次設定為0並返回。
-
呼叫unpark(thread)方法後,就會將thread執行緒的許可permit設定成1(注意多次呼叫unpark方法,不會累加,permit值還是1)會自動喚醒thread執行緒,即之前阻塞中的LockSupport.park()方法會立即返回。
-
LockSupport和每個使用它的執行緒都有一個許可(permit)關聯。permit相當於1,0的開關,預設是0,
呼叫一次unpark就將0變成1,
呼叫一次park會消費permit,也就是將1變成o,同時park立即返回。
如再次呼叫park會變成阻塞(因為permit為零了會阻塞在這裡,一直到permit變為1),這時呼叫unpark會把permit置為1。
每個執行緒都有一個相關的permit, permit最多隻有一個,重複呼叫unpark也不會積累憑證。 -
形象的理解
執行緒阻塞需要消耗憑證(permit),這個憑證最多隻有1個。
當呼叫park方法時
*如果有憑證,則會直接消耗掉這個憑證然後正常退出;
*如果無憑證,就必須阻塞等待憑證可用;
而unpark則相反,它會增加一個憑證,但憑證最多隻能有1個,累加無效。
我們用LockSupport來測試下之前的異常場景
異常情況1
無同步程式碼塊
public class LockSupportDemo3 {
public static void main(String[] args) {
/**
LockSupport:俗稱 鎖中斷
LockSupport它的解決的痛點
1。LockSupport不用持有鎖塊,不用加鎖,程式效能好,
2。不需要等待和喚醒的先後順序,不容易導致卡死
*/
Thread t1 = new Thread(() -> {
System.out.println(Thread.currentThread().getName() + "\t ----begin-時間:" + System.currentTimeMillis());
LockSupport.park();//阻塞當前執行緒
System.out.println(Thread.currentThread().getName() + "\t ----被喚醒-時間:" + System.currentTimeMillis());
}, "t1");
t1.start();
LockSupport.unpark(t1);
System.out.println(Thread.currentThread().getName() + "\t 通知t1...");
}
}
結果:
t1 ----begin-時間:1603376148147
t1 ----被喚醒-時間:1603376148147
main 通知t1...
Process finished with exit code 0
沒有問題
異常情況2
先喚醒,再阻塞(等待)。
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "\t ----begin-時間:" + System.currentTimeMillis());
LockSupport.park();//阻塞當前執行緒
System.out.println(Thread.currentThread().getName() + "\t ----被喚醒-時間:" + System.currentTimeMillis());
}, "t1");
t1.start();
LockSupport.unpark(t1);
System.out.println(Thread.currentThread().getName() + "\t 通知t1...");
}
結果:
main 通知t1...
t1 ----begin-時間:1603376257183
t1 ----被喚醒-時間:1603376257183
Process finished with exit code 0
可以看到,如果你先喚醒了。那麼後面的LockSupport.park();
就相當於瞬間被喚醒了,不會和之前一樣程式卡死。為什麼呢?結合之前分析的流程
1、先執行unpark,將許可證由0變為1
2、然後park來了發現許可證此時為0(也就是有許可證),那麼他就不會阻塞,馬上就往後執行。同時消耗許可證(也就是將1又變為0)。
AQS
AQS是什麼?
**字面意思:**抽象的佇列同步器
**技術翻譯:**是用來構建鎖或者其它同步器元件的重量級基礎框架及整個JUC體系的基石, 通過內建的FIFO佇列來完成資源獲取執行緒的排隊工作,並通過一個int類變數state
表示持有鎖的狀態。
AbstractOwnableSynchronizer
AbstractQueuedLongSynchronizer
AbstractQueuedSynchronizer
上面幾個都是AQS,但是通常地: AbstractQueuedSynchronizer簡稱為AQS。
AQS是一個抽象的父類,可以將其理解為一個框架。基於AQS這個框架,我們可以實現多種同步器,比如下方圖中的幾個Java內建的同步器。同時我們也可以基於AQS框架實現我們自己的同步器以滿足不同的業務場景需求。
AQS能幹嘛?
加鎖會導致阻塞:有阻塞就需要排隊,實現排隊必然需要有某種形式的佇列來進行管理
1、搶到資源的執行緒直接使用辦理業務,搶佔不到資源的執行緒的必然涉及一種排隊等候機制,搶佔資源失敗的執行緒繼續去等待(類似辦理視窗都滿了,暫時沒有受理視窗的顧客只能去候客區排隊等候),仍然保留獲取鎖的可能且獲取鎖流程仍在繼續(候客區的顧客也在等著叫號,輪到了再去受理視窗辦理業務)。
2、既然說到了排隊等候機制,那麼就一定 會有某種佇列形成,這樣的佇列是什麼資料結構呢?
3、如果共享資源被佔用,就需要一定的阻塞等待喚醒機制來保證鎖分配。這個機制主要用的是CLH佇列的變體實現的,將暫時獲取不到鎖的執行緒加入到佇列中,這個佇列就是AQS的抽象表現。它將請求共享資源的執行緒封裝成佇列的結點(Node) ,通過CAS、自旋以及LockSuport.park()的方式,維護state變數的狀態,使併發達到同步的效果。
AQS獨佔模式(以ReentrantLock 原始碼為例)
AQS結構
// 頭結點,你直接把它當做當前持有鎖的執行緒 可能是最好理解的。實際上可能略有出入,往下看分析即可
private transient volatile Node head;
// 阻塞的尾節點,每個新的節點進來,都插入到最後,也就形成了一個連結串列
private transient volatile Node tail;
// 這個是最重要的,代表當前鎖的狀態,0代表沒有被佔用,大於 0 代表有執行緒持有當前鎖
// 這個值可以大於 1,是因為鎖可以重入,每次重入都加上 1
private volatile int state;
// 代表當前持有獨佔鎖的執行緒,舉個最重要的使用例子,因為鎖可以重入
// reentrantLock.lock()可以巢狀呼叫多次,所以每次用這個來判斷當前執行緒是否已經擁有了鎖
// if (currentThread == getExclusiveOwnerThread()) {state++}
private transient Thread exclusiveOwnerThread; //繼承自AbstractOwnableSynchronizer
Node類結構
static final class Node {
// 標識節點當前在共享模式下
static final Node SHARED = new Node();
// 標識節點當前在獨佔模式下
static final Node EXCLUSIVE = null;
// ======== 下面的幾個int常量是給waitStatus用的 ===========
/** waitStatus value to indicate thread has cancelled */
// 程式碼此執行緒取消了爭搶這個鎖
static final int CANCELLED = 1;
/** waitStatus value to indicate successor's thread needs unparking */
// 官方的描述是,其表示當前node的後繼節點對應的執行緒需要被喚醒
static final int SIGNAL = -1;
/** waitStatus value to indicate thread is waiting on condition */
// 等待condition喚醒
static final int CONDITION = -2;
/**
* waitStatus value to indicate the next acquireShared should
* unconditionally propagate
*/
// 共享模式同步狀態獲取講會無條件的傳播下去(共享模式下,該欄位才會使用)
static final int PROPAGATE = -3;
// ===============-2和-3用的不多,暫時不分析======================================
// 取值為上面的1、-1、-2、-3,或者0(以後會講到,waitStatus初始值為0)
// 這麼理解,暫時只需要知道如果這個值 大於0 代表此執行緒取消了等待,
// ps: 半天搶不到鎖,不搶了,ReentrantLock是可以指定timeouot的
volatile int waitStatus;
// 前驅節點的引用
volatile Node prev;
// 後繼節點的引用
volatile Node next;
// 這個就是執行緒本尊
volatile Thread thread;
}
Node 的資料結構其實也挺簡單的,就是 thread + waitStatus + pre + next 四個屬性而已,大家先要有這個概念在心裡。
AQS佇列基本結構
注意排隊佇列,不包括head(也就是後文要說的哨兵節點)。
開始
package com.youth.guiguthirdquarter.AQS;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;
/**
* @Author: youthlql-呂
* @Date: 2020/10/25 21:59
* <p>
* 功能描述:
*/
public class AQSDemo {
public static void main(String[] args) {
ReentrantLock lock = new ReentrantLock();
//帶入一個銀行辦理業務的案例來模擬我們的AQS如何進行執行緒的管理和通知喚醒機制
//3個執行緒模擬3個來銀行網點,受理視窗辦理業務的顧客
//A顧客就是第一個顧客,此時受理視窗沒有任何人,A可以直接去辦理
new Thread(() -> {
lock.lock();
try{
System.out.println("-----A thread come in");
try { TimeUnit.MINUTES.sleep(20); }catch (Exception e) {e.printStackTrace();}
}finally {
lock.unlock();
}
},"A").start();
//第二個顧客,第二個執行緒---》由於受理業務的視窗只有一個(只能一個執行緒持有鎖),此時B只能等待,
//進入候客區
new Thread(() -> {
lock.lock();
try{
System.out.println("-----B thread come in");
}finally {
lock.unlock();
}
},"B").start();
//第三個顧客,第三個執行緒---》由於受理業務的視窗只有一個(只能一個執行緒持有鎖),此時C只能等待,
//進入候客區
new Thread(() -> {
lock.lock();
try{
System.out.println("-----C thread come in");
}finally {
lock.unlock();
}
},"C").start();
}
}
以這樣的一個實際例子說明。
非公平鎖lock()加鎖
lock()
static final class NonfairSync extends Sync {
private static final long serialVersionUID = 7316153563782823691L;
final void lock() {
/*
1、非公平鎖不公平的第一個原因就出現在這裡。剛準備加鎖的執行緒,這裡會用CAS搶一下鎖(也就是通過
看state的狀態)。如果搶成功了就呼叫setExclusiveOwnerThread,設定當前持有獨佔鎖的執行緒為本
執行緒。
*/
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
//如果搶鎖失敗就走入這個流程,搶鎖失敗說明當前鎖已經被佔用了
acquire(1);
}
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
}
//相當於只要呼叫了這個方法,說明執行緒獨佔鎖成功
protected final void setExclusiveOwnerThread(Thread thread) {
exclusiveOwnerThread = thread;
}
A執行緒剛進來的時候,AQS的head和tail節點都還沒有被初始化,則會被預設初始化為null。並且state預設初始化為0。
1、A執行緒進去視窗辦理業務,此時state == 0,那麼CAS就直接成功了,並且把sate改為1。然後呼叫下setExclusiveOwnerThread
,就直接結束了。【加鎖成功,直接返回】
B執行緒
1、接著B執行緒去視窗辦理業務,因為之前A執行緒把state變為了1,那麼B執行緒在進行第一個if-CAS判斷就會失敗。所以就走到了else分支,呼叫acquire(1)
方法。
C執行緒
因為A執行緒佔用著鎖,C執行緒執行邏輯和B一樣。(後續假設C進行加鎖時間在B後面一點)
acquire()和tryAcquire()
/*
1、acquire()方法來自父類AQS,我們看到,這個方法,如果tryAcquire(arg) 返回true, 也就結束了。
否則,acquireQueued方法會將執行緒壓到佇列中。
*/
public final void acquire(int arg) { // 此時 arg == 1
/*
1、首先呼叫tryAcquire(1)一下,名字上就知道,這個只是試一試。因為有可能直接就成功了呢,也就不需要進隊 列排隊了。
2、有可能成功的情況就是,在走到這一步的時候,前面佔鎖的執行緒剛好釋放鎖
*/
if (!tryAcquire(arg) &&
// tryAcquire(arg)沒有成功,這個時候需要把當前執行緒掛起,放到阻塞佇列中。
acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) {
selfInterrupt();
}
}
/*
1、上面的tryAcquire裡會直接呼叫ReentrantLock類的nonfairTryAcquire方法,
2、嘗試直接獲取鎖,返回值是boolean,代表是否獲取到鎖
3、有兩種情況會返回true:
1.沒有執行緒在等待鎖
2.重入鎖,執行緒本來就持有鎖,也就可以理所當然可以直接獲取
*/
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
/*
1、state == 0 此時此刻沒有執行緒持有鎖
2、前面也說了有可能成功的情況就是,在走到這一步的時候,前面佔鎖的執行緒剛好釋放鎖
*/
if (c == 0) {
//那就用CAS嘗試一下,成功了就獲取到鎖了。
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
// 會進入這個else if分支,說明是重入鎖了,需要操作:state=state+1
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
B執行緒
1、B執行緒最終走進了nonfairTryAcquire()
方法,但是因為A還在佔鎖(佔著處理視窗state),所以此時state為1,B執行緒走到else if分支進行判斷。
2、B執行緒發現已經佔有鎖的執行緒不是自己,說明不是重入鎖,也不會進入else if分支。最終返回fasle,回到tryAcquire
,準備掛起執行緒。
C執行緒
因為A執行緒佔用著鎖,C執行緒執行邏輯和B一樣
addWaiter()
/*
1、假設tryAcquire(arg) 返回false,那麼程式碼將執行:acquireQueued(addWaiter(Node.EXCLUSIVE), arg),這個方法,首先需要執行:addWaiter(Node.EXCLUSIVE)
2、此方法的作用是把執行緒包裝成node,同時進入到佇列中。引數mode此時是Node.EXCLUSIVE,代表獨佔模式
3、以下幾行程式碼想把當前node加到連結串列的最後面去,也就是進到佇列的最後
*/
private Node addWaiter(Node mode) {
Node node = new Node(Thread.currentThread(), mode);
//得到尾節點(head和tail在沒有初始化前都是null,沒有初始化的時候也說明佇列為空)
Node pred = tail;
//佇列不為空時(即之前已經初始化過了),會進入下面這個分支,此時只需要將新的node加入隊尾
if (pred != null) {
// 將當前的隊尾節點,設定為自己的前驅
node.prev = pred;
// 用CAS把自己設定為隊尾, 如果成功後,tail == node 了,這個節點成為排隊佇列新的尾巴
if (compareAndSetTail(pred, node)) {
/*
1、進到這裡說明設定成功,當前node==tail, 將自己與之前的隊尾相連,上面已經有 node.prev = pred,加上下面這句,也就實現了和之前的尾節點雙向連線了
*/
pred.next = node;
// 執行緒入隊了,可以返回了
return node;
}
}
/*
1、仔細看看上面的程式碼,有兩種情況會走到這裡
1、pred==null(說明佇列是空的)
2、CAS設定隊尾失敗(有執行緒在競爭入隊)
*/
enq(node);
return node;
}
之前說了A執行緒剛進來的時候,AQS的head和tail節點都還沒有被初始化,則會被預設初始化為null
B執行緒
1、B執行緒進入addWaiter()
,發現pred == null,直接進入enq()
C執行緒
1、【前面說了C在B後面】,C執行緒進來後和B不一樣,因為B在後面已經設定了tail指標。那麼C執行緒在判斷的時候pred 就不是null,就直接進入了if分支
2、C在if邏輯裡準備入隊,進行相應設定後,變成下面這樣。
enq()
/*
1、採用空的for迴圈,以自旋的方式入隊,到這個方法只有兩種可能:佇列為空,或者有執行緒競爭入隊【上面說過】
2、自旋在這邊的語義是:CAS設定tail過程中,競爭一次競爭不到,我就多次競爭,總會排到的
*/
private Node enq(final Node node) {
for (;;) {
Node t = tail;
/*
1、進入這個分支,說明是佇列為空的這種情況,那麼就準備初始化一個空的節點(new Node())作為排隊佇列 的head。
*/
if (t == null) { // Must initialize
/*
1、初始化head節點,前面說過 head 和 tail 初始化的時候都是 null 的。
2、還是一步CAS,因為可能是很多執行緒同時進來呢
*/
if (compareAndSetHead(new Node()))
/*
1、注意這裡傳的引數是new Node(),說明是一個空的節點(並不是我們B執行緒封裝的節點,這 個空節點只作為佔位符,稱作傀儡節點或者哨兵節點)。這個時候head節點的waitStatus==0, 看 new Node()構造方法就知道了。注意:new Node()雖然是空節點,但他不是null
2、這個時候有了head,但是tail還是null,設定一下,把tail指向head,放心,馬上就有執行緒要 來了,到時候tail就要被搶了
3、注意:這裡只是設定了tail=head,這裡可沒return哦。所以,設定完了以後,繼續for迴圈, 下次就到下面的else分支了
*/
tail = head;
} else {
/*
1、下面幾行,和上一個方法 addWaiter 是一樣的,只是這個套在無限迴圈裡,就是將當前執行緒排到 隊尾,有執行緒競爭的話排不上重複排,直到排上了再return 【這裡看不懂的話就看下面的例子】 */
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
B執行緒
第一輪迴圈
1、B執行緒進入enq()。首先發現t == tail 依然為null,那麼就直接進入if分支。
2、進入if分支後,呼叫compareAndSetHead(new Node())
準備初始化head節點。注意這裡傳的引數是new Node()
,說明是一個空的節點(並不是我們B執行緒封裝的節點,這個空節點只作為佔位符,稱作傀儡節點或者哨兵節點),然後將head賦值給tail。
補充:雙向連結串列中,第一個節點為虛節點(也叫哨兵節點),其實並不儲存任何資訊,只是佔位。 真正的第一個有資料的節點,是從第二個節點開始的。
此時佇列變成了下面的樣子:
3、然後if結束之後,繼續空的for迴圈,B執行緒開始了第二輪迴圈。
第二輪迴圈
1、第二次迴圈再過來的時候,t == tail,但此時tail不再為null,所以進入else分支。
2、node.prev = t
,進入if之後,讓B節點的prev指標指向t,然後compareAndSetTail(t, node)
設定尾節點
3、CAS設定尾節點成功之後,執行if裡的邏輯
acquireQueued()
/*
1、現在,又回到這段程式碼了
if (!tryAcquire(arg)
&& acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
2、acquireQueued這個方法,引數node,經過addWaiter(Node.EXCLUSIVE),此時已經進入排隊佇列隊尾
3、注意一下:如果acquireQueued(addWaiter(Node.EXCLUSIVE), arg))返回true的話,意味著上面這段程式碼將 進入selfInterrupt()
4、這個方法非常重要,真正的執行緒掛起,然後被喚醒後去獲取鎖,都在這個方法裡了
*/
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor();
/*
1、p == head 說明當前節點雖然進到了排隊佇列,但是是佇列的第一個,因為它的前驅是head(或者說 是哨兵節點,因為head指向了哨兵節點)
2、注意,佇列不包含head節點,head一般指的是佔有鎖的執行緒,head後面的才稱為排隊佇列
3、所以當前節點可以去試搶一下鎖
4、這裡我們說一下,為什麼可以去試試:它是排隊佇列隊頭,所以作為隊頭,可以去試一試能不能拿到 鎖,因為可能之前的執行緒已經釋放鎖了。如果嘗試成功,那它就不需要被掛起,直接拿鎖,效率會高
5、tryAcquire已經分析過了, 忘記了請往前看一下,就是簡單用CAS試操作一下state
*/
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC,這個後面釋放鎖的時候會講
failed = false;
return interrupted;
}
/*
1、到這裡,說明上面的if分支沒有成功。
1、要麼當前node本來就不是隊頭,
2、要麼就是tryAcquire(arg)沒有搶贏別人,繼續往下看
*/
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
//tryAcquire()方法拋異常時,failed為true,會取消當前節點的排隊。
if (failed)
cancelAcquire(node);//取消排隊
}
}
B執行緒
1、進入acquireQueued()
後,發現也是一個空迴圈。首先通過node.predecessor()
得到B節點的前一個節點P,也就是哨兵節點。
2、p == head為true。然後if裡再次執行tryAcquire(arg)
拿一次鎖【流程前面已經分析過了,不重複了】。因為A執行緒任然持有鎖,所以最終結果B節點tryAcquire
失敗。準備掛起執行緒
shouldParkAfterFailedAcquire()
/*
1、會到這裡就是沒有搶到鎖唄,這個方法說的是:"當前執行緒沒有搶到鎖,是否需要掛起當前執行緒?"
第一個引數是前驅節點,第二個引數才是代表當前執行緒的節點
*/
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus;
// 前驅節點的 waitStatus == -1 ,說明前驅節點狀態正常,當前執行緒需要掛起,直接可以返回true
if (ws == Node.SIGNAL)
return true;
/*
1、前驅節點 waitStatus大於0 ,之前說過,大於0說明前驅節點取消了排隊。
2、這裡需要知道這點:進入阻塞佇列排隊的執行緒會被掛起,而喚醒的操作是由前驅節點完成的。所以下面這塊程式碼說 的是將當前節點的prev指向waitStatus<=0的節點,簡單說,就是為了找個好爹,因為你還得依賴它來喚醒呢,如 果前驅節點取消了排隊,找前驅節點的前驅節點做爹,往前遍歷總能找到一個好爹的。
*/
if (ws > 0) {
/*
* Predecessor was cancelled. Skip over predecessors and
* indicate retry.
*/
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
/*
* waitStatus must be 0 or PROPAGATE. Indicate that we
* need a signal, but don't park yet. Caller will need to
* retry to make sure it cannot acquire before parking.
*/
/*
1、如果進入到這個分支意味著什麼,前驅節點的waitStatus不等於-1和1,那也就是隻可能是0,-2,-3
在我們前面的原始碼中,都沒有看到有設定waitStatus的,所以每個新的node入隊時,waitStatu都是0
2、正常情況下,前驅節點是之前的 tail,那麼它的 waitStatus 應該是 0,用CAS將前驅節點的 waitStatus設定為Node.SIGNAL(也就是-1),表示我後面有節點需要被喚醒。
3、這裡可以簡單說下 waitStatus 中 SIGNAL(-1) 狀態的意思,Doug Lea 註釋的是:代表後繼節點需要 被喚醒。也就是說這個 waitStatus 其實代表的不是自己的狀態,而是後繼節點的狀態,我們知道,每個 node 在入隊的時候,都會把前驅節點的狀態改為 SIGNAL,然後阻塞,等待被前驅喚醒。這裡涉及的是兩個問 題:有執行緒取消了排隊、喚醒操作。其實本質是一樣的,讀者也可以順著 “waitStatus代表後繼節點的狀態” 這種思路去看一遍原始碼。
*/
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
// 這個方法返回 false,那麼會再走一次 for 循序,然後再次進來此方法,此時會從第一個分支返回 true
return false;
}
/*
1、private static boolean shouldParkAfterFailedAcquire(Node pred, Node node)
這個方法結束根據返回值我們簡單分析下:
1、如果返回true, 說明前驅節點的waitStatus==-1,是正常情況,那麼當前執行緒需要被掛起,等待以後被喚醒
我們也說過,以後是被前驅節點喚醒,就等著前驅節點拿到鎖,然後釋放鎖的時候叫你好了
2、如果返回false, 說明當前不需要被掛起,為什麼呢?往後看
需要跳回到前面這個方法
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
*/
B執行緒
第一次迴圈
1、B執行緒的前驅節點是哨兵節點(ws == 0), 所以最終走了else分支,執行了 compareAndSetWaitStatus(pred, ws, Node.SIGNAL)
方法。將哨兵節點的compareAndSetWaitStatus
值變為了-1
2、返回false,返回到acquireQueued()
進行第二次迴圈【不再贅述】。
第二次迴圈
1、此時B執行緒的前驅節點–哨兵節點的ws == -1。那麼此方法返回true,準備執行parkAndCheckInterrupt
parkAndCheckInterrupt()
/*
1、如果shouldParkAfterFailedAcquire(p, node)返回true,那麼需要執行parkAndCheckInterrupt():
這個方法很簡單,因為前面返回true,所以需要掛起執行緒,這個方法就是負責掛起執行緒的,
2、這裡用了LockSupport.park(this)來掛起執行緒,然後就停在這裡了,等待被喚醒=======
*/
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);
return Thread.interrupted();
}
/*
1、接下來說說如果shouldParkAfterFailedAcquire(p, node)返回false的情況
2、仔細看shouldParkAfterFailedAcquire(p, node),我們可以發現,其實第一次進來的時候,一般都不會返回 true的,原因很簡單,前驅節點的waitStatus=-1是依賴於後繼節點設定的。也就是說,我都還沒給前驅設定-1呢,怎麼 可能是true呢,但是要看到,這個方法是套在迴圈裡的,所以第二次進來的時候狀態就是-1了。
3、解釋下為什麼shouldParkAfterFailedAcquire(p, node)返回false的時候不直接掛起執行緒:
主要是為了應對在經過這個方法後,node已經是head的直接後繼節點了。
4、假設返回fasle的時候,node已經是head的直接後繼節點了,但是你直接掛起了執行緒,就要走別人喚醒你的那幾步代 碼。那這裡完全可以重新走一遍for迴圈,直接嘗試下獲取鎖,可能會更快。注意是可能,不代表一定,因為你也無法確定 unparkSuccessor釋放鎖,通知後繼節點這個方法執行的快慢。但是你多嘗試一次獲取鎖,總歸是快的。
for (;;) {
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return;
}
*/
}
到這一步,B執行緒才算真正的入隊坐穩了。B執行緒在這裡阻塞,或者說掛起。
非公平鎖lock()解鎖
然後,就是還需要介紹下喚醒的動作了。我們知道,正常情況下,如果執行緒沒獲取到鎖,執行緒會被 LockSupport.park(this);
掛起停止,等待被喚醒。
release()和tryRelease()
// 喚醒的程式碼還是比較簡單的,你如果上面加鎖的都看懂了,下面都不需要看就知道怎麼回事了
public void unlock() {
sync.release(1);
}
//AQS類的方法
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
//h是哨兵節點
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
// 回到ReentrantLock看tryRelease方法
protected final boolean tryRelease(int releases) {
int c = getState() - releases;
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
// 是否完全釋放鎖
boolean free = false;
// 其實就是重入的問題,如果c==0,也就是說沒有巢狀鎖了,可以釋放了,否則還不能釋放掉
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}
unparkSuccessor()
// 喚醒後繼節點,從上面呼叫處知道,引數node是head頭結點(或者說是哨兵節點,因為本身head就指向了哨兵節點)
private void unparkSuccessor(Node node) {
int ws = node.waitStatus;
// 如果head節點當前waitStatus<0, 將其修改為0
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
/*
1、下面的程式碼就是喚醒後繼節點,但是有可能後繼節點取消了等待(waitStatus==1)從隊尾往前找,找到 waitStatus<=0的所有節點中排在最前面的
*/
Node s = node.next;
if (s == null || s.waitStatus > 0) {
s = null;
// 從後往前找,仔細看程式碼,不必擔心中間有節點取消(waitStatus==1)的情況
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
s = t;
}
if (s != null)
// 喚醒執行緒
LockSupport.unpark(s.thread);
}
B執行緒
1、哨兵節點的後一個節點就是B節點,B節點的waitStatus == 0,所以就直接走喚醒執行緒那一步了。
喚醒之後
喚醒執行緒以後,被喚醒的執行緒將從以下程式碼中繼續往前走:
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this); // 剛剛執行緒被掛起在這裡了
return Thread.interrupted();
}
// 又回到這個方法了:acquireQueued(final Node node, int arg),這個時候,node的前驅是head了
返回這個方法進行第三次迴圈
//node還是B節點
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor();
//A執行緒走了,B就可以tryacquire成功
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC,
failed = false;
return interrupted;
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
1、B執行緒tryAcquire()
成功之後就佔有了state,也就是拿到了鎖。
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
protected final void setExclusiveOwnerThread(Thread thread) {
exclusiveOwnerThread = thread;
}
2、此時state那裡有B執行緒的引用exclusiveOwnerThread
,佇列裡也有B執行緒的引用,需要把佇列裡的多餘引用給GC掉。
3、AQS採用的是將head指向B節點成為新的哨兵節點,舊的哨兵節點因為沒有任何引用指向了,慢慢就會被GC掉。
公平鎖和非公平鎖
看了上面的原始碼,這個知識點應該是可以很輕鬆理解的。公平鎖和非公平鎖在原始碼層次只有幾處不一樣。
構造
ReentrantLock 預設採用非公平鎖,除非你在構造方法中傳入引數 true 。
public ReentrantLock() {
// 預設非公平鎖
sync = new NonfairSync();
}
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
非公平鎖的 lock 方法
static final class NonfairSync extends Sync {
final void lock() {
// 1、和公平鎖相比,這裡會直接先進行一次CAS,成功就返回了。這是第一處不一樣
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
// AbstractQueuedSynchronizer類的acquire(int arg)方法
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
}
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
// 這裡沒有對佇列進行判斷,直接CAS搶,這是第二點不一樣【對比請看下方公平鎖的lock】
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
公平鎖的 lock 方法
static final class FairSync extends Sync {
final void lock() {
acquire(1);
}
// AbstractQueuedSynchronizer類的acquire(int arg)方法
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
// 2、和非公平鎖相比,這裡多了一個判斷:是否有執行緒在佇列列等待,有我就不搶了
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
}
推薦
相關文章
- Java併發之AQS原始碼分析(二)JavaAQS原始碼
- java 併發程式設計-AQS原始碼分析Java程式設計AQS原始碼
- Java併發之AQS詳解JavaAQS
- 【Java併發】【AQS鎖】鎖在原始碼中的應用JavaAQS原始碼
- Java併發包原始碼學習系列:ReentrantReadWriteLock讀寫鎖解析Java原始碼
- JDK併發AQS系列(三)JDKAQS
- JDK併發AQS系列(五)JDKAQS
- JDK併發AQS系列(二)JDKAQS
- JDK併發AQS系列(一)JDKAQS
- AQS原始碼閱讀AQS原始碼
- 併發程式設計之:AQS原始碼解析程式設計AQS原始碼
- 併發工具類:Semaphore原始碼解讀原始碼
- Java併發:深入淺出AQS之獨佔鎖模式原始碼分析JavaAQS模式原始碼
- java併發神器 AQS(AbstractQueuedSynchronizer)JavaAQS
- java併發程式設計系列:牛逼的AQS(上)Java程式設計AQS
- java併發程式設計系列:牛逼的AQS(下)Java程式設計AQS
- Java併發包原始碼學習系列:同步元件CountDownLatch原始碼解析Java原始碼元件CountDownLatch
- Java併發包原始碼學習系列:同步元件CyclicBarrier原始碼解析Java原始碼元件
- Java併發包原始碼學習系列:同步元件Semaphore原始碼解析Java原始碼元件
- Java併發指南10:Java 讀寫鎖 ReentrantReadWriteLock 原始碼分析Java原始碼
- Java併發(5)- ReentrantLock與AQSJavaReentrantLockAQS
- Java併發之AQS原理剖析JavaAQS
- Java併發包原始碼學習系列:基於CAS非阻塞併發佇列ConcurrentLinkedQueue原始碼解析Java原始碼佇列
- Vite 原始碼解讀系列(圖文結合) —— 構建篇Vite原始碼
- Vite 原始碼解讀系列(圖文結合) —— 外掛篇Vite原始碼
- 深入理解Java併發框架AQS系列(四):共享鎖(Shared Lock)Java框架AQS
- 最強幹貨:Java併發之AQS原理詳解JavaAQS
- Java併發指南7:JUC的核心類AQS詳解JavaAQS
- vuex 原始碼:原始碼系列解讀總結Vue原始碼
- Vite 原始碼解讀系列(圖文結合) —— 本地開發伺服器篇Vite原始碼伺服器
- Java併發(6)- CountDownLatch、Semaphore與AQSJavaCountDownLatchAQS
- Java併發——AbstractQueuedSynchronizer(AQS)同步器JavaAQS
- [Java併發]AQS的可重入性JavaAQS
- 併發-AQSAQS
- Java併發包原始碼學習系列:ReentrantLock可重入獨佔鎖詳解Java原始碼ReentrantLock
- Java併發包原始碼學習系列:執行緒池ThreadPoolExecutor原始碼解析Java原始碼執行緒thread
- Java併發包原始碼學習系列:JDK1.8的ConcurrentHashMap原始碼解析Java原始碼JDKHashMap
- Java併發包原始碼學習系列:執行緒池ScheduledThreadPoolExecutor原始碼解析Java原始碼執行緒thread