基於AQS實現自己的同步工具

前言

文本已收錄至我的GitHub倉庫,歡迎Star:https://github.com/bin392328206/six-finger
「種一棵樹最好的時間是十年前,其次是現在」
我知道很多人不玩「qq」了,但是懷舊一下,歡迎加入六脈神劍Java菜鳥學習羣,羣聊號碼:「549684836」 鼓勵大家在技術的路上寫博客

絮叨

這篇文章,基於子路老師的AQS,在這之前我對AQS,對它的源碼大部分是似懂非懂的感覺,但是看了子路老師的文章,好像頓悟了,哈哈,其實怎麼說呢?AQS這個大家一定要會,併發編程的基礎就是AQS,只要你對併發有點點基礎,小六六這篇文檔保證讓你有對併發編程,對AQS有不一樣的認知。

分析

Java的內置鎖一直都是備受爭議的,在JDK 1.6之前,synchronized這個重量級鎖其性能一直都是較爲低下,雖然在1.6後,進行大量的鎖優化策略,但是與Lock相比synchronized還是存在一些缺陷的:雖然synchronized提供了便捷性的隱式獲取鎖釋放鎖機制(基於JVM機制),但是它卻缺少了獲取鎖與釋放鎖的可操作性,可中斷、超時獲取鎖,且它爲獨佔式在高併發場景下性能大打折扣。目前大部分公司的jdk肯定是1.8,所以我想說的是,對於sync 和 lock 的使用,都是看各種的場景,並沒有好壞之說。

如何自己來實現一個同步

我們知道AQS就是一個資源競爭排隊的一種實現手段,就好比做地鐵排隊進站,買票排隊一樣,那我們自己如何來實現這種同步的機制呢?

通過自旋來實現

volatile int status=0;//標識---是否有線程在同步塊-----是否有線程上鎖成功
void lock(){
	while(!compareAndSet(0,1)){
	}
	//lock
}
void unlock(){
	status=0;
}
boolean compareAndSet(int except,int newValue){
	//cas操作,修改status成功則返回true
}


上面的僞代碼其實很簡單,我們來一個線程就去自旋去拿鎖,採用一直自旋的方式+valatile+cas的方式就可以實現資源的安全訪問了,但是我們看上面的僞代碼其實是有一點點缺點的,就是自旋的時候會非常消耗cpu的資源,如果競爭的線程非常多的話,那系統肯定扛不住了,那我們有沒有其他的改進方案呢?

缺點:耗費cpu資源。沒有競爭到鎖的線程會一直佔用cpu資源進行cas操作,假如一個線程獲得鎖後要花費Ns處理業務邏輯,那另外一個線程就會白白的花費Ns的cpu資源 解決思路:讓得不到鎖的線程讓出CPU

yield+自旋實現同步

volatile int status=0;
void lock(){
	while(!compareAndSet(0,1)){
     yield();//自己實現
	}
	//lock


}
void unlock(){
	status=0;
}


要解決自旋鎖的性能問題必須讓競爭鎖失敗的線程不空轉,而是在獲取不到鎖的時候能把cpu資源給讓出來,yield()方法就能讓出cpu資源,當線程競爭鎖失敗時,會調用yield方法讓出cpu。自旋+yield的方式並沒有完全解決問題,當系統只有兩個線程競爭鎖時,yield是有效的。需要注意的是該方法只是當前讓出cpu,有可能操作系統下次還是選擇運行該線程,比如裏面有2000個線程,想想會有什麼問題?

sleep+自旋方式實現同步

volatile int status=0;
void lock(){
	while(!compareAndSet(0,1)){
		sleep(10);
	}
	//lock


}
void unlock(){
	status=0;
}


缺點:sleep的時間爲什麼是10?怎麼控制呢?很多時候就算你是調用者本身其實你也不知道這個時間是多少

park+自旋方式實現同步

volatile int status=0;
Queue parkQueue;//集合 數組  list


void lock(){
	while(!compareAndSet(0,1)){
		//
		park();
	}
	//lock    10分鐘
   。。。。。。
   unlock()
}


void unlock(){
	lock_notify();
}


void park(){
	//將當期線程加入到等待隊列
	parkQueue.add(currentThread);
	//將當期線程釋放cpu  阻塞
	releaseCpu();
}
void lock_notify(){
	//得到要喚醒的線程頭部線程
	Thread t=parkQueue.header();
	//喚醒等待線程
	unpark(t);
}


看看這種方案,我們採用了一個Queue 和一個volatile的 status +cas+ pack(unsafe) 來實現這個同步隊列,事實上我們的JUC下面的lock的實現原理的思路其實也是差不多,但是人家的複雜層度就是不一樣了

ReentrantLock源碼分析之上鎖過程

AQS(AbstractQueuedSynchronizer)類的設計主要代碼(具體參考源碼)

private transient volatile Node head; //隊首
private transient volatile Node tail;//尾
private volatile int state;//鎖狀態,加鎖成功則爲1,重入+1 解鎖則爲0


AQS當中的隊列示意圖

Node類的設計

public class Node{
    volatile Node prev;
    volatile Node next;
    volatile Thread thread;
    volatile int waitStatus; //Node 對於普通同步節點,該字段初始化爲0,對於條件節點,該字段初始化爲* CONDITION。使用CAS *(或在可能時進行無條件的易失性寫操作)對其進行修改
}


上鎖過程重點

鎖對象:其實就是ReentrantLock的實例對象,下文應用代碼第一行中的lock對象就是所謂的鎖 自由狀態:自由狀態表示鎖對象沒有被別的線程持有,計數器爲0 計數器:再lock對象中有一個字段state用來記錄上鎖次數,比如lock對象是自由狀態則state爲0,如果大於零則表示被線程持有了,當然也有重入那麼state則>1 waitStatus:僅僅是一個狀態而已;ws是一個過渡狀態,在不同方法裏面判斷ws的狀態做不同的處理,所以ws=0有其存在的必要性 tail:隊列的隊尾 head:隊列的對首 ts:第二個給lock加鎖的線程 tf:第一個給lock加鎖的線程 tc:當前給線程加鎖的線程 tl:最後一個加鎖的線程 tn:隨便某個線程 當然這些線程有可能重複,比如第一次加鎖的時候tf=tc=tl=tn 節點:就是上面的Node類的對象,裏面封裝了線程,所以某種意義上node就等於一個線程

首先一個簡單的應用

跟着一個個往下跟下去,小六六建議大家一起跟着dubug 最少設計3個線程 分別看看 交替進行,和併發進行的區別,每種線程下的隊列 和各個線程封裝的Node的變化。

 final ReentrantLock lock = new ReentrantLock(true);
 Thread t1= new Thread("t1"){
     @Override
     public void run() {
         lock.lock();
         logic();
         lock.unlock();
     }
 };
t1.start();


公平鎖lock方法的源碼分析

final void lock() {
    acquire(1);//1------標識加鎖成功之後改變的值
}


非公平鎖的looc方法

final void lock() {
	if (compareAndSetState(0, 1))
		setExclusiveOwnerThread(Thread.currentThread());
	else
		 acquire(1);
}


公平和非公平的邏輯圖

公平鎖的上鎖是必須判斷自己是不是需要排隊;而非公平鎖是直接進行CAS修改計數器看能不能加鎖成功;如果加鎖不成功則乖乖排隊(調用acquire);所以不管公平還是不公平;只要進到了AQS隊列當中那麼他就會排隊;一朝排隊;永遠排隊記住這點

這個之前也有一篇文章寫的非常好了

  • 一張圖讀懂公平與非公平

acquire方法方法源碼分析

接下來,其實非公平鎖,只是比公平鎖多了一次機會去獲取鎖,如果它這次失敗了,那麼它也只能說乖乖去排隊,那麼接下來需要調用的就是acquire 方法了

public final void acquire(int arg) {
    //tryAcquire(arg)嘗試加鎖,如果加鎖失敗則會調用acquireQueued方法加入隊列去排隊,如果加鎖成功則不會調用
    //acquireQueued方法下文會有解釋
    //加入隊列之後線程會立馬park,等到解鎖之後會被unpark,醒來之後判斷自己是否被打斷了;被打斷下次分析
    //爲什麼需要執行這個方法?下文解釋
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}


acquire方法中 有三個核心方法,我們先大致來了解一下他們

  • tryAcquire(arg) 嘗試加鎖,如果加鎖失敗則會調用acquireQueued方法加入隊列去排隊,如果加鎖成功則不會調用

  • addWaiter 添加到AQS的隊列中

  • acquireQueued 到隊列中之後,它需要做的事情

acquire方法首先會調用tryAcquire方法,注意tryAcquire的結果做了取反

tryAcquire方法源碼分析

protected final boolean tryAcquire(int acquires) {
    //獲取當前線程
    final Thread current = Thread.currentThread();
    //獲取lock對象的上鎖狀態,如果鎖是自由狀態則=0,如果被上鎖則爲1,大於1表示重入
    int c = getState();
    if (c == 0) {//沒人佔用鎖--->我要去上鎖----1、鎖是自由狀態
        //hasQueuedPredecessors,判斷自己是否需要排隊這個方法比較複雜,
        //下面我會單獨介紹,如果不需要排隊則進行cas嘗試加鎖,如果加鎖成功則把當前線程設置爲擁有鎖的線程
        //繼而返回true
        if (!hasQueuedPredecessors() &&
            compareAndSetState(0, acquires)) {
            //設置當前線程爲擁有鎖的線程,方面後面判斷是不是重入(只需把這個線程拿出來判斷是否當前線程即可判斷重入)    
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    //如果C不等於0,而且當前線程不等於擁有鎖的線程則不會進else if 直接返回false,加鎖失敗
    //如果C不等於0,但是當前線程等於擁有鎖的線程則表示這是一次重入,那麼直接把狀態+1表示重入次數+1
    //那麼這裏也側面說明了reentrantlock是可以重入的,因爲如果是重入也返回true,也能lock成功
    else if (current == getExclusiveOwnerThread()) {
        int nextc = c + acquires;
        if (nextc < 0)
            throw new Error("Maximum lock count exceeded");
        setState(nextc);
        return true;
    }
    return false;
}


總結一下他們的幾點功能

  • 首先拿到當前線程和AQS status的狀態。如果AQS狀態爲0  說明沒人用,中間還要一個方法是判斷自己是否需要排隊,如果不需要排隊的話,那我當前線程是不是可以直接cas去獲取鎖,對吧

  • 如果status 不等於0,並且佔有的線程和當前線程不一樣,這個地方就是可重複鎖的判斷,如果是一樣的就是可重複鎖,如果不是,直接返回false,去排隊。

hasQueuedPredecessors判斷是否需要排隊的源碼分析

接着上面的方法看,如果tryacquire的status的狀態爲0的時候,我們是不是說我們需要判斷一下我們是否怕排隊,排隊這個還是有點複雜,我們來看看大佬的分析

public final boolean hasQueuedPredecessors() {
    Node t = tail; 
    Node h = head;
    Node s;
    /**
     * 下面提到的所有不需要排隊,並不是字面意義,我實在想不出什麼詞語來描述這個“不需要排隊”;不需要排隊有兩種情況
     * 一:隊列沒有初始化,不需要排隊,不需要排隊,不需要排隊;直接去加鎖,但是可能會失敗;爲什麼會失敗呢?
     * 假設兩個線程同時來lock,都看到隊列沒有初始化,都認爲不需要排隊,都去進行CAS修改計數器;有一個必然失敗
     * 比如t1先拿到鎖,那麼另外一個t2則會CAS失敗,這個時候t2就會去初始化隊列,並排隊
     *
     * 二:隊列被初始化了,但是tc過來加鎖,發覺隊列當中第一個排隊的就是自己;比如重入;
     * 那麼什麼叫做第一個排隊的呢?下面解釋了,很重要往下看;
     * 這個時候他也不需要排隊,不需要排隊,不需要排隊;爲什麼不需要排對?
     * 因爲隊列當中第一個排隊的線程他會去嘗試獲取一下鎖,因爲有可能這個時候持有鎖鎖的那個線程可能釋放了鎖;
     * 如果釋放了就直接獲取鎖執行。但是如果沒有釋放他就會去排隊,
     * 所以這裏的不需要排隊,不是真的不需要排隊
     *
     * h != t 判斷首不等於尾這裏要分三種情況
     * 1、隊列沒有初始化,也就是第一個線程tf來加鎖的時候那麼這個時候隊列沒有初始化,
     * h和t都是null,那麼這個時候判斷不等於則不成立(false)那麼由於是&&運算後面的就不會走了,
     * 直接返回false表示不需要排隊,而前面又是取反(if (!hasQueuedPredecessors()),所以會直接去cas加鎖。
     * ----------第一種情況總結:隊列沒有初始化沒人排隊,那麼我直接不排隊,直接上鎖;合情合理、有理有據令人信服;
     * 好比你去火車站買票,服務員都閒的蛋疼,整個隊列都沒有形成;沒人排隊,你直接過去交錢拿票
     *
     * 2、隊列被初始化了,後面會分析隊列初始化的流程,如果隊列被初始化那麼h!=t則成立;(不絕對,還有第3中情況)
     * h != t 返回true;但是由於是&&運算,故而代碼還需要進行後續的判斷
     * (有人可能會疑問,比如隊列初始化了;裏面只有一個數據,那麼頭和尾都是同一個怎麼會成立呢?
     * 其實這是第3種情況--對頭等於對尾;但是這裏先不考慮,我們假設現在隊列裏面有大於1個數據)
     * 大於1個數據則成立;繼續判斷把h.next賦值給s;s有是對頭的下一個Node,
     * 這個時候s則表示他是隊列當中參與排隊的線程而且是排在最前面的;
     * 爲什麼是s最前面不是h嘛?誠然h是隊列裏面的第一個,但是不是排隊的第一個;下文有詳細解釋
     * 因爲h也就是對頭對應的Node對象或者線程他是持有鎖的,但是不參與排隊;
     * 這個很好理解,比如你去買車票,你如果是第一個這個時候售票員已經在給你服務了,你不算排隊,你後面的纔算排隊;
     * 隊列裏面的h是不參與排隊的這點一定要明白;參考下面關於隊列初始化的解釋;
     * 因爲h要麼是虛擬出來的節點,要麼是持有鎖的節點;什麼時候是虛擬的呢?什麼時候是持有鎖的節點呢?下文分析
     * 然後判斷s是否等於空,其實就是判斷隊列裏面是否只有一個數據;
     * 假設隊列大於1個,那麼肯定不成立(s==null---->false),因爲大於一個Node的時候h.next肯定不爲空;
     * 由於是||運算如果返回false,還要判斷s.thread != Thread.currentThread();這裏又分爲兩種情況
     *        2.1 s.thread != Thread.currentThread() 返回true,就是當前線程不等於在排隊的第一個線程s;
     *              那麼這個時候整體結果就是h!=t:true; (s==null false || s.thread != Thread.currentThread() true  最後true)
     *              結果:true && true 方法最終放回true,所以需要去排隊
     *              其實這樣符合情理,試想一下買火車票,隊列不爲空,有人在排隊;
     *              而且第一個排隊的人和現在來參與競爭的人不是同一個,那麼你就乖乖去排隊
     *        2.2 s.thread != Thread.currentThread() 返回false 表示當前來參與競爭鎖的線程和第一個排隊的線程是同一個線程
     *             這個時候整體結果就是h!=t---->true; (s==null false || s.thread != Thread.currentThread() false-----> 最後false)
     *            結果:true && false 方法最終放回false,所以不需要去排隊
     *            不需要排隊則調用 compareAndSetState(0, acquires) 去改變計數器嘗試上鎖;
     *            這裏又分爲兩種情況(日了狗了這一行代碼;有同學課後反應說子路老師老師老是說這個AQS難,
     *            你現在仔細看看這一行代碼的意義,真的不簡單的)
     *             2.2.1  第一種情況加鎖成功?有人會問爲什麼會成功啊,如這個時候h也就是持有鎖的那個線程執行完了
     *                      釋放鎖了,那麼肯定成功啊;成功則執行 setExclusiveOwnerThread(current); 然後返回true 自己看代碼
     *             2.2.2  第二種情況加鎖失敗?有人會問爲什麼會失敗啊。假如這個時候h也就是持有鎖的那個線程沒執行完
     *                       沒釋放鎖,那麼肯定失敗啊;失敗則直接返回false,不會進else if(else if是相對於 if (c == 0)的)
     *                      那麼如果失敗怎麼辦呢?後面分析;
     *
     *----------第二種情況總結,如果隊列被初始化了,而且至少有一個人在排隊那麼自己也去排隊;但是有個插曲;
     * ----------他會去看看那個第一個排隊的人是不是自己,如果是自己那麼他就去嘗試加鎖;嘗試看看鎖有沒有釋放
     *----------也合情合理,好比你去買票,如果有人排隊,那麼你乖乖排隊,但是你會去看第一個排隊的人是不是你女朋友;
     *----------如果是你女朋友就相當於是你自己(這裏實在想不出現實世界關於重入的例子,只能用男女朋友來替代);
     * --------- 你就叫你女朋友看看售票員有沒有搞完,有沒有輪到你女朋友,因爲你女朋友是第一個排隊的
     * 疑問:比如如果在在排隊,那麼他是park狀態,如果是park狀態,自己怎麼還可能重入啊。
     * 希望有同學可以想出來爲什麼和我討論一下,作爲一個菜逼,希望有人教教我
     *  
     * 
     * 3、隊列被初始化了,但是裏面只有一個數據;什麼情況下才會出現這種情況呢?ts加鎖的時候裏面就只有一個數據?
     * 其實不是,因爲隊列初始化的時候會虛擬一個h作爲頭結點,tc=ts作爲第一個排隊的節點;tf爲持有鎖的節點
     * 爲什麼這麼做呢?因爲AQS認爲h永遠是不排隊的,假設你不虛擬節點出來那麼ts就是h,
     *  而ts其實需要排隊的,因爲這個時候tf可能沒有執行完,還持有着鎖,ts得不到鎖,故而他需要排隊;
     * 那麼爲什麼要虛擬爲什麼ts不直接排在tf之後呢,上面已經時說明白了,tf來上鎖的時候隊列都沒有,他不進隊列,
     * 故而ts無法排在tf之後,只能虛擬一個thread=null的節點出來(Node對象當中的thread爲null);
     * 那麼問題來了;究竟什麼時候會出現隊列當中只有一個數據呢?假設原隊列裏面有5個人在排隊,當前面4個都執行完了
     * 輪到第五個線程得到鎖的時候;他會把自己設置成爲頭部,而尾部又沒有,故而隊列當中只有一個h就是第五個
     * 至於爲什麼需要把自己設置成頭部;其實已經解釋了,因爲這個時候五個線程已經不排隊了,他拿到鎖了;
     * 所以他不參與排隊,故而需要設置成爲h;即頭部;所以這個時間內,隊列當中只有一個節點
     * 關於加鎖成功後把自己設置成爲頭部的源碼,後面會解析到;繼續第三種情況的代碼分析
     * 記得這個時候隊列已經初始化了,但是隻有一個數據,並且這個數據所代表的線程是持有鎖
     * h != t false 由於後面是&&運算,故而返回false可以不參與運算,整個方法返回false;不需要排隊
     *
     *
     *-------------第三種情況總結:如果隊列當中只有一個節點,而這種情況我們分析了,
     *-------------這個節點就是當前持有鎖的那個節點,故而我不需要排隊,進行cas;嘗試加鎖
     *-------------這是AQS的設計原理,他會判斷你入隊之前,隊列裏面有沒有人排隊;
     *-------------有沒有人排隊分兩種情況;隊列沒有初始化,不需要排隊
     *--------------隊列初始化了,但是隻有一個節點,也是沒人排隊,自己先也不排隊
     *--------------只要認定自己不需要排隊,則先嚐試加鎖;加鎖失敗之後再排隊;
     *--------------再一次解釋了不需要排隊這個詞的歧義性
     *-------------如果加鎖失敗了,在去park,下文有詳細解釋這樣設計源碼和原因
     *-------------如果持有鎖的線程釋放了鎖,那麼我能成功上鎖
     *
     **/
    return h !=  &&
        ((s = h.next) == null || s.thread != Thread.currentThread());
}


代碼只有2三行,但是因爲是併發編程,它的意義和情況就是非常多,這就是併發編程的魅力,我們來總結一下

  • h!=t 這個是啥意思呢?這個AQS隊首 和隊尾爲null 那麼返回的就是false 說明不要排隊,這就是第一種情況 也就是當我們隊列還沒初始化的時候,我們是不需要排隊的, 好比你去火車站買票,服務員都閒的蛋疼,整個隊列都沒有形成;沒人排隊,你直接過去交錢拿票,如果都沒人排隊,難道你還會去排隊,傻傻的等着服務員來問你是否需要買票?

  • 小六六看得也很懵逼,這行代碼要考慮的太多了。延申的情況太多了上面的註釋大家可以好好,當status爲0時,判斷自己是否需要排隊的場景還是很牛逼的

到此我們已經解釋完了!tryAcquire(arg)方法,爲了方便我再次貼一下代碼

public final void acquire(int arg) {
    //tryAcquire(arg)嘗試加鎖,如果加鎖失敗則會調用acquireQueued方法加入隊列去排隊,如果加鎖成功則不會調用
    //acquireQueued方法下文會有解釋
    //加入隊列之後線程會立馬park,等到解鎖之後會被unpark,醒來之後判斷自己是否被打斷了
    //爲什麼需要執行這個方法?下次解釋
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}


acquireQueued(addWaiter(Node.exclusive),arg))方法解析

public final void acquire(int arg) {
    //tryAcquire(arg)嘗試加鎖,如果加鎖失敗則會調用acquireQueued方法加入隊列去排隊,如果加鎖成功則不會調用
    //acquireQueued方法下文會有解釋
    //加入隊列之後線程會立馬park,等到解鎖之後會被unpark,醒來之後判斷自己是否被打斷了;被打斷下次分析
    //爲什麼需要執行這個方法?下文解釋
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}

如果代碼能執行到這裏說tc需要排隊 需要排隊有兩種情況—換言之代碼能夠執行到這裏有兩種情況:1、tf持有了鎖,並沒有釋放,所以tc來加鎖的時候需要排隊,但這個時候—隊列並沒有初始化 2、tn(無所謂哪個線程,反正就是一個線程)持有了鎖,那麼由於加鎖tn!=tf(tf是屬於第一種情況,我們現在不考慮tf了),所以隊列是一定被初始化了的,tc來加鎖,那麼隊列當中有人在排隊,故而他也去排隊

這邊說明什麼呢?就是如果代碼還能往下走,那在當前來說肯定是需要去排隊的,對吧,那要排隊,我們想一想第一步是要幹嘛,當然是用當前線程去封裝一個Node 然後通過這個Node把它放到AQS裏面 並且把AQS 本身的head tail 改變之類的。

addWaiter(Node.EXCLUSIVE)源碼分析

private Node addWaiter(Node mode) {
    //由於AQS隊列當中的元素類型爲Node,故而需要把當前線程tc封裝成爲一個Node對象,下文我們叫做nc
    Node node = new Node(Thread.currentThread(), mode);
    //tail爲對尾,賦值給pred 
    Node pred = tail;
    //判斷pred是否爲空,其實就是判斷對尾是否有節點,其實只要隊列被初始化了對尾肯定不爲空,
    //假設隊列裏面只有一個元素,那麼對尾和對首都是這個元素
    //換言之就是判斷隊列有沒有初始化
    //上面我們說過代碼執行到這裏有兩種情況,1、隊列沒有初始化和2、隊列已經初始化了
    //pred不等於空表示第二種情況,隊列被初始化了,如果是第二種情況那比較簡單
   //直接把當前線程封裝的nc的上一個節點設置成爲pred即原來的對尾
   //繼而把pred的下一個節點設置爲當nc,這個nc自己成爲對尾了
    if (pred != null) {
        //直接把當前線程封裝的nc的上一個節點設置成爲pred即原來的對尾,對應 10行的註釋
        node.prev = pred;
        //這裏需要cas,因爲防止多個線程加鎖,確保nc入隊的時候是原子操作
        if (compareAndSetTail(pred, node)) {
            //繼而把pred的下一個節點設置爲當nc,這個nc自己成爲對尾了 對應第11行註釋
            pred.next = node;
            //然後把nc返回出去,方法結束
            return node;
        }
    }
    //如果上面的if不成了就會執行到這裏,表示第一種情況隊列並沒有初始化---下面解析這個方法
    enq(node);
    //返回nc
    return node;
}




private Node enq(final Node node) {//這裏的node就是當前線程封裝的node也就是nc
    //死循環
    for (;;) {
        //對尾複製給t,上面已經說過隊列沒有初始化,
        //故而第一次循環t==null(因爲是死循環,因此強調第一次,後面可能還有第二次、第三次,每次t的情況肯定不同)
        Node t = tail;
        //第一次循環成了成立
        if (t == null) { // Must initialize
            //new Node就是實例化一個Node對象下文我們稱爲nn,
            //調用無參構造方法實例化出來的Node裏面三個屬性都爲null,可以關聯Node類的結構,
            //compareAndSetHead入隊操作;把這個nn設置成爲隊列當中的頭部,cas防止多線程、確保原子操作;
            //記住這個時候隊列當中只有一個,即nn
            if (compareAndSetHead(new Node()))
                //這個時候AQS隊列當中只有一個元素,即頭部=nn,所以爲了確保隊列的完整,設置頭部等於尾部,即nn即是頭也是尾
                //然後第一次循環結束;接着執行第二次循環,第二次循環代碼我寫在了下面,接着往下看就行
                tail = head;
        } else {
            node.prev = t;
            if (compareAndSetTail(t, node)) {
                t.next = node;
                return t;
            }
        }
    }
}




//爲了方便 第二次循環我再貼一次代碼來對第二遍循環解釋
private Node enq(final Node node) {//這裏的node就是當前線程封裝的node也就是nc
    //死循環
    for (;;) {
        //對尾複製給t,由於第二次循環,故而tail==nn,即new出來的那個node
        Node t = tail;
        //第二次循環不成立
        if (t == null) { // Must initialize
            if (compareAndSetHead(new Node()))
                tail = head;
        } else {
            //不成立故而進入else
            //首先把nc,當前線程所代表的的node的上一個節點改變爲nn,因爲這個時候nc需要入隊,入隊的時候需要把關係維護好
            //所謂的維護關係就是形成鏈表,nc的上一個節點只能爲nn,這個很好理解
            node.prev = t;
            //入隊操作--把nc設置爲對尾,對首是nn,
            if (compareAndSetTail(t, node)) {
                //上面我們說了爲了維護關係把nc的上一個節點設置爲nn
                //這裏同樣爲了維護關係,把nn的下一個節點設置爲nc
                t.next = node;
                //然後返回t,即nn,死循環結束,enq(node);方法返回
                //這個返回其實就是爲了終止循環,返回出去的t,沒有意義
                return t;
            }
        }
    }
}


  //這個方法已經解釋完成了
  enq(node);
  //返回nc,不管哪種情況都會返回nc;到此addWaiter方法解釋完成
  return node;




//再次貼出node的結構方便大家查看
public class Node{
    volatile Node prev;
    volatile Node next;
    volatile Thread thread;
}






-------------------總結:addWaiter方法就是讓nc入隊-並且維護隊列的鏈表關係,但是由於情況複雜做了不同處理
-------------------主要針對隊列是否有初始化,沒有初始化則new一個新的Node nn作爲對首,nn裏面的線程爲null
-------------------接下來分析acquireQueued方法


總結一下這個方法,這個方法其實就是封裝一下AQS的隊列,addWaiter方法就是讓nc入隊-並且維護隊列的鏈表關係,但是由於情況複雜做了不同處理,主要針對隊列是否有初始化,沒有初始化則new一個新的Node nn作爲對首,nn裏面的線程爲null,接下來分析acquireQueued方法,這裏面有一個點,就是在隊首的元素 自旋2次再去看看有沒有機會拿到鎖, 就是enq方法 爲啥是2次 不是3次 4次 這個是作者絕對的,還有爲啥要去嘗試拿鎖,原因就是爲了不park。

acquireQueued方法的源碼分析

final boolean acquireQueued(final Node node, int arg) {//這裏的node 就是當前線程封裝的那個node 下文叫做nc
    //記住標誌很重要
    boolean failed = true;
    try {
        //同樣是一個標誌
        boolean interrupted = false;
        //死循環
        for (;;) {
            //獲取nc的上一個節點,有兩種情況;1、上一個節點爲頭部;2上一個節點不爲頭部
            final Node p = node.predecessor();
            //如果nc的上一個節點爲頭部,則表示nc爲隊列當中的第二個元素,爲隊列當中的第一個排隊的Node;
            //這裏的第一和第二不衝突;我上文有解釋;
            //如果nc爲隊列當中的第二個元素,第一個排隊的則調用tryAcquire去嘗試加鎖---關於tryAcquire看上面的分析
            //只有nc爲第二個元素;第一個排隊的情況下才會嘗試加鎖,其他情況直接去park了,
            //因爲第一個排隊的執行到這裏的時候需要看看持有有鎖的線程有沒有釋放鎖,釋放了就輪到我了,就不park了
            //有人會疑惑說開始調用tryAcquire加鎖失敗了(需要排隊),這裏爲什麼還要進行tryAcquire不是重複了嗎?
            //其實不然,因爲第一次tryAcquire判斷是否需要排隊,如果需要排隊,那麼我就入隊;
            //當我入隊之後我發覺前面那個人就是第一個,持有鎖的那個,那麼我不死心,再次問問前面那個人搞完沒有
            //如果他搞完了,我就不park,接着他搞我自己的事;如果他沒有搞完,那麼我則在隊列當中去park,等待別人叫我
            //但是如果我去排隊,發覺前面那個人在睡覺,前面那個人都在睡覺,那麼我也睡覺把---------------好好理解一下
            if (p == head && tryAcquire(arg)) {
                //能夠執行到這裏表示我來加鎖的時候,鎖被持有了,我去排隊,進到隊列當中的時候發覺我前面那個人沒有park,
                //前面那個人就是當前持有鎖的那個人,那麼我問問他搞完沒有
                //能夠進到這個裏面就表示前面那個人搞完了;所以這裏能執行到的機率比較小;但是在高併發的世界中這種情況真的需要考慮
                //如果我前面那個人搞完了,我nc得到鎖了,那麼前面那個人直接出隊列,我自己則是對首;這行代碼就是設置自己爲對首
                setHead(node);
                //這裏的P代表的就是剛剛搞完事的那個人,由於他的事情搞完了,要出隊;怎麼出隊?把鏈表關係刪除
                p.next = null; // help GC
                //設置表示---記住記加鎖成功的時候爲false
                failed = false;
                //返回false;爲什麼返回false?下次博客解釋---比較複雜和加鎖無關
                return interrupted;
            }
            //進到這裏分爲兩種情況
            //1、nc的上一個節點不是頭部,說白了,就是我去排隊了,但是我上一個人不是隊列第一個
            //2、第二種情況,我去排隊了,發覺上一個節點是第一個,但是他還在搞事沒有釋放鎖
            //不管哪種情況這個時候我都需要park,park之前我需要把上一個節點的狀態改成park狀態
            //這裏比較難以理解爲什麼我需要去改變上一個節點的park狀態呢?每個node都有一個狀態,默認爲0,表示無狀態
            //-1表示在park;當時不能自己把自己改成-1狀態?爲什麼呢?因爲你得確定你自己park了纔是能改爲-1;
            //不然你自己改成自己爲-1;但是改完之後你沒有park那不就騙人?
            //你對外宣佈自己是單身狀態,但是實際和劉宏斌私下約會;這有點坑人
            //所以只能先park;在改狀態;但是問題你自己都park了;完全釋放CPU資源了,故而沒有辦法執行任何代碼了,
            //所以只能別人來改;故而可以看到每次都是自己的後一個節點把自己改成-1狀態
            //關於shouldParkAfterFailedAcquire這個方法的源碼下次博客繼續講吧
            if (shouldParkAfterFailedAcquire(p, node) &&
                //改上一個節點的狀態成功之後;自己park;到此加鎖過程說完了
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}






public final void acquire(int arg) {
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}


這個方法就很重要了,所有要排隊的隊列,都是在這個方法裏面調用了 unsafe 方法的park方法來 出讓cpu ,但是呢這裏面的情況也畢竟多,我們來稍微看看

  • 如果它發現它前面的那個節點是頭節點,並且它去嘗試獲取鎖,還拿到了,就好比買票,你知道你前面只有一個人了,那麼過幾分鐘就會去問下你買好了嘛,然後它剛好買完,它就說好了,你去買吧,爲啥要這樣呢,就是有一種情況,前面哪個人它買完票了,但是它剛好要告訴下一個人的時候,剛好有人打個電話來了,那它就是打電話去了,如果你不主動去問,那麼你就得等它電話打完。就是在代碼中,我們unpack 和改status 當改完 status 的時候 剛好cpu的分片被分走了,我去就是這麼巧,所以 我發現源碼的每一行代碼真的是精髓,太牛逼了。就是下面這段代碼的意義

  if (p == head && tryAcquire(arg)) {
                //能夠執行到這裏表示我來加鎖的時候,鎖被持有了,我去排隊,進到隊列當中的時候發覺我前面那個人沒有park,
                //前面那個人就是當前持有鎖的那個人,那麼我問問他搞完沒有
                //能夠進到這個裏面就表示前面那個人搞完了;所以這裏能執行到的機率比較小;但是在高併發的世界中這種情況真的需要考慮
                //如果我前面那個人搞完了,我nc得到鎖了,那麼前面那個人直接出隊列,我自己則是對首;這行代碼就是設置自己爲對首
                setHead(node);
                //這裏的P代表的就是剛剛搞完事的那個人,由於他的事情搞完了,要出隊;怎麼出隊?把鏈表關係刪除
                p.next = null; // help GC
                //設置表示---記住記加鎖成功的時候爲false
                failed = false;
                //返回false;爲什麼返回false?下次博客解釋---比較複雜和加鎖無關
                return interrupted;
            }

我們再來看看shouldParkAfterFailedAcquire(Node, Node)

private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
    int ws = pred.waitStatus;//拿到前驅的狀態
    if (ws == Node.SIGNAL)
        //如果已經告訴前驅拿完號後通知自己一下,那就可以安心休息了
        return true;
    if (ws > 0) {
        /*
         * 如果前驅放棄了,那就一直往前找,直到找到最近一個正常等待的狀態,並排在它的後邊。
         * 注意:那些放棄的結點,由於被自己“加塞”到它們前邊,它們相當於形成一個無引用鏈,稍後就會被保安大叔趕走了(GC回收)!
         */
        do {
            node.prev = pred = pred.prev;
        } while (pred.waitStatus > 0);
        pred.next = node;
    } else {
         //如果前驅正常,那就把前驅的狀態設置成SIGNAL,告訴它拿完號後通知自己一下。有可能失敗,人家說不定剛剛釋放完呢!
        compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
    }
    return false;
}

ReentrantLock解鎖解析

首先調用模板方法sync.release(1)

由於公平鎖和非公平鎖的解鎖過程一樣,這裏ReentrantLock都會調用sync.release(1) 方法。

public void unlock() {
        sync.release(1);
    }


模板方法 release(1) 分析

public final boolean release(int arg) {
		// 嘗試釋放鎖,如果成功則會調用unparkSuccessor(h) 下文解析
        if (tryRelease(arg)) {
            Node h = head;
           // 如果隊頭不爲空且後繼線程存在某種狀態,那麼會喚醒第一個排隊線程
            if (h != null && h.waitStatus != 0)
                unparkSuccessor(h);
            return true;
        }
        return false;
    }


tryRelease(arg) 嘗試解鎖方法分析

protected final boolean tryRelease(int releases) {
			// 持有鎖 state-1 ,判斷之後是否爲自由狀態 (c=0?)
			// 可能會是重入,那麼 state-1,之後也不能釋放鎖
            int c = getState() - releases;
            // 當前解鎖線程不是持有鎖的線程,這種情況很少出現
            if (Thread.currentThread() != getExclusiveOwnerThread())
                throw new IllegalMonitorStateException();
            boolean free = false;
            // 如果 state 可以爲自由狀態,那麼就讓持有線程變爲null
            // 解鎖成功,返回true
            if (c == 0) {
                free = true;
                setExclusiveOwnerThread(null);
            }
            // 最後還要重新設置 state 的狀態
            setState(c);
            return free;
        }


unparkSuccessor(h) 喚醒隊列等待線程方法分析

private void unparkSuccessor(Node node) {
		// 這裏獲取隊頭結點的下一個結點線程的狀態 ws
        int ws = node.waitStatus;
        // 如果 ws=-1(SIGNAL),那麼就置爲初始狀態 ws=0
        if (ws < 0)
            compareAndSetWaitStatus(node, ws, 0);
        // 獲取隊頭結點的下一個線程結點,也就是第一個排隊結點
        Node s = node.next;
        // 隊列只有一個隊頭結點 或者 ws>0 s結點線程需要從同步隊列中取消等待
        if (s == null || s.waitStatus > 0) {
            s = null;
            // 從隊列中隊尾往前,找到距離head最近 ws<=0 的結點(線程狀態正常)
            // 如果遇到結點爲null或者結點爲隊頭,那麼就結束循環
            for (Node t = tail; t != null && t != node; t = t.prev)
                if (t.waitStatus <= 0)
                    s = t;
        }
        // 如果找到的這個結點不爲null,那就喚醒它
        if (s != null)
            LockSupport.unpark(s.thread);
    }


結尾

小六六覺得,越跟到後面越看不懂,後面子路老師的Thread.interrupted() 講的也很牛逼 我現在的水平不夠,看得蒙,反正小六六的水平肯定是夠的,但是我覺得學習是螺旋上升的,一次學不會,那就等下次,很多人覺得說學一個東西,就一定要一次性全部弄懂,反正小六六沒有那麼牛逼,我只能說每次學習有不同的收穫,多學習幾次這樣子。AQS的源碼是真的優秀。後面還會繼續學習的 B站搜AQS 播放量最多的就是它的,如果對AQS和小六六一樣一知半解的可以去學習學習。

日常求贊

好了各位,以上就是這篇文章的全部內容了,能看到這裏的人呀,都是「真粉」

創作不易,各位的支持和認可,就是我創作的最大動力,我們下篇文章見

六脈神劍 | 文 【原創】如果本篇博客有任何錯誤,請批評指教,不勝感激 !

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章