1. 程式人生 > 實用技巧 >AQS ReentrantLock

AQS ReentrantLock

Java的內建鎖一直都是備受爭議的,在JDK

1.6之前,synchronized這個重量級鎖其效能一直都是較為低下,雖然在1.6後,進行大量的鎖優化策略,但是與Lock相比synchronized還是存在一些缺陷的:雖然synchronized提供了便捷性的隱式獲取鎖釋放鎖機制(基於JVM機制),但是它卻缺少了獲取鎖與釋放鎖的可操作性,可中斷、超時獲取鎖,且它為獨佔式在高併發場景下效能大打折扣。

————————————————

如何自己來實現一個同步

自旋實現一個同步

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

缺點:耗費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);
}

這種方法就比較完美,當然我寫的都虛擬碼,我看看大師是如何利用這種機制來實現同步的;JDK的JUC包下面ReentrantLock類的原理就是利用了這種機制;這種方法就比較完美,當然我寫的都虛擬碼,我看看大師是如何利用這種機制來實現同步的;JDK的JUC包下面ReentrantLock類的原理就是利用了這種機制;

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;
}

上鎖過程重點

鎖物件:其實就是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就等於一個執行緒

首先一個簡單的應用

 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方法方法原始碼分析

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

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;
}
hasQueuedPredecessors判斷是否需要排隊的原始碼分析

這裡需要記住一點,整個方法如果最後返回false,則去加鎖,如果返回true則不加鎖,因為這個方法被取反了

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 != t &&
        ((s = h.next) == null || s.thread != Thread.currentThread());
}

到此我們已經解釋完了!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))方法解析

如果程式碼能執行到這裡說tc需要排隊

需要排隊有兩種情況—換言之程式碼能夠執行到這裡有兩種情況:

1、tf持有了鎖,並沒有釋放,所以tc來加鎖的時候需要排隊,但這個時候—佇列並沒有初始化

2、tn(無所謂哪個執行緒,反正就是一個執行緒)持有了鎖,那麼由於加鎖tn!=tf(tf是屬於第一種情況,我們現在不考慮tf了),所以佇列是一定被初始化了的,tc來加鎖,那麼隊列當中有人在排隊,故而他也去排隊

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方法

acquireQueued(addWaiter(Node.exclusive),arg))經過上面的解析之後可以理解成為acquireQueued(nc,arg))

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();
}

加鎖過程總結:

如果是第一個執行緒tf,那麼和佇列無關,執行緒直接持有鎖。並且也不會初始化佇列,如果接下來的執行緒都是交替執行,那麼永遠和AQS佇列無關,都是直接執行緒持有鎖,如果發生了競爭,比如tf持有鎖的過程中T2來lock,那麼這個時候就會初始化AQS,初始化AQS的時候會在佇列的頭部虛擬一個Thread為NULL的Node,因為隊列當中的head永遠是持有鎖的那個node(除了第一次會虛擬一個,其他時候都是持有鎖的那個執行緒鎖封裝的node),現在第一次的時候持有鎖的是tf而tf不在隊列當中所以虛擬了一個node節點,隊列當中的除了head之外的所有的node都在park,當tf釋放鎖之後unpark某個(基本是隊列當中的第二個,為什麼是第二個呢?前面說過head永遠是持有鎖的那個node,當有時候也不會是第二個,比如第二個被cancel之後,至於為什麼會被cancel,不在我們討論範圍之內,cancel的條件很苛刻,基本不會發生)node之後,node被喚醒,假設node是t2,那麼這個時候會首先把t2變成head(sethead),在sethead方法裡面會把t2代表的node設定為head,並且把node的Thread設定為null,為什麼需要設定null?其實原因很簡單,現在t2已經拿到鎖了,node就不要排隊了,那麼node對Thread的引用就沒有意義了。所以佇列的head裡面的Thread永遠為null

————————————————

原文連結:https://blog.csdn.net/java_lyvee/article/details/98966684

Java的內建鎖一直都是備受爭議的,在JDK1.6之前,synchronized這個重量級鎖其效能一直都是較為低下,雖然在1.6後,進行大量的鎖優化策略,但是與Lock相比synchronized還是存在一些缺陷的:雖然synchronized提供了便捷性的隱式獲取鎖釋放鎖機制(基於JVM機制),但是它卻缺少了獲取鎖與釋放鎖的可操作性,可中斷、超時獲取鎖,且它為獨佔式在高併發場景下效能大打折扣。
如何自己來實現一個同步自旋實現一個同步volatile int status=0;//標識---是否有執行緒在同步塊-----是否有執行緒上鎖成功void lock(){while(!compareAndSet(0,1)){}//lock}void unlock(){status=0;}boolean compareAndSet(int except,int newValue){//cas操作,修改status成功則返回true}123456789101112缺點:耗費cpu資源。沒有競爭到鎖的執行緒會一直佔用cpu資源進行cas操作,假如一個執行緒獲得鎖後要花費Ns處理業務邏輯,那另外一個執行緒就會白白的花費Ns的cpu資源解決思路:讓得不到鎖的執行緒讓出CPU
yield+自旋實現同步volatile int status=0;void lock(){while(!compareAndSet(0,1)){ yield();//自己實現}//lock
}void unlock(){status=0;}1234567891011要解決自旋鎖的效能問題必須讓競爭鎖失敗的執行緒不空轉,而是在獲取不到鎖的時候能把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;}1234567891011缺點: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);}1234567891011121314151617181920212223242526272829這種方法就比較完美,當然我寫的都虛擬碼,我看看大師是如何利用這種機制來實現同步的;JDK的JUC包下面ReentrantLock類的原理就是利用了這種機制;
ReentrantLock原始碼分析之上鎖過程AQS(AbstractQueuedSynchronizer)類的設計主要程式碼(具體參考原始碼)private transient volatile Node head; //隊首private transient volatile Node tail;//尾private volatile int state;//鎖狀態,加鎖成功則為1,重入+1 解鎖則為0123AQS當中的佇列示意圖

Node類的設計public class Node{ volatile Node prev; volatile Node next; volatile Thread thread;}12345上鎖過程重點鎖物件:其實就是ReentrantLock的例項物件,下文應用程式碼第一行中的lock物件就是所謂的鎖自由狀態:自由狀態表示鎖物件沒有被別的執行緒持有,計數器為0計數器:再lock物件中有一個欄位state用來記錄上鎖次數,比如lock物件是自由狀態則state為0,如果大於零則表示被執行緒持有了,當然也有重入那麼state則>1waitStatus:僅僅是一個狀態而已;ws是一個過渡狀態,在不同方法裡面判斷ws的狀態做不同的處理,所以ws=0有其存在的必要性tail:佇列的隊尾 head:佇列的對首 ts:第二個給lock加鎖的執行緒 tf:第一個給lock加鎖的執行緒 tc:當前給執行緒加鎖的執行緒tl:最後一個加鎖的執行緒 tn:隨便某個執行緒當然這些執行緒有可能重複,比如第一次加鎖的時候tf=tc=tl=tn節點:就是上面的Node類的物件,裡面封裝了執行緒,所以某種意義上node就等於一個執行緒
首先一個簡單的應用final ReentrantLock lock = new ReentrantLock(true);Thread t1= new Thread("t1"){ @Override public void run() { lock.lock(); logic(); lock.unlock(); }};t1.start();12345678910公平鎖lock方法的原始碼分析final void lock() { acquire(1);//1------標識加鎖成功之後改變的值}123非公平鎖的looc方法final void lock() {if (compareAndSetState(0, 1))setExclusiveOwnerThread(Thread.currentThread());else acquire(1);}123456下面給出他們的程式碼執行邏輯的區別圖


公平鎖的上鎖是必須判斷自己是不是需要排隊;而非公平鎖是直接進行CAS修改計數器看能不能加鎖成功;如果加鎖不成功則乖乖排隊(呼叫acquire);所以不管公平還是不公平;只要進到了AQS隊列當中那麼他就會排隊;一朝排隊;永遠排隊記住這點
acquire方法方法原始碼分析public final void acquire(int arg) { //tryAcquire(arg)嘗試加鎖,如果加鎖失敗則會呼叫acquireQueued方法加入佇列去排隊,如果加鎖成功則不會呼叫 //acquireQueued方法下文會有解釋 //加入佇列之後執行緒會立馬park,等到解鎖之後會被unpark,醒來之後判斷自己是否被打斷了;被打斷下次分析 //為什麼需要執行這個方法?下文解釋 if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) selfInterrupt();}123456789acquire方法首先會呼叫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;}12345678910111213141516171819202122232425262728hasQueuedPredecessors判斷是否需要排隊的原始碼分析這裡需要記住一點,整個方法如果最後返回false,則去加鎖,如果返回true則不加鎖,因為這個方法被取反了
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 != t && ((s = h.next) == null || s.thread != Thread.currentThread());}123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293到此我們已經解釋完了!tryAcquire(arg)方法,為了方便我再次貼一下程式碼
public final void acquire(int arg) { //tryAcquire(arg)嘗試加鎖,如果加鎖失敗則會呼叫acquireQueued方法加入佇列去排隊,如果加鎖成功則不會呼叫 //acquireQueued方法下文會有解釋 //加入佇列之後執行緒會立馬park,等到解鎖之後會被unpark,醒來之後判斷自己是否被打斷了 //為什麼需要執行這個方法?下次解釋 if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) selfInterrupt();}123456789acquireQueued(addWaiter(Node.exclusive),arg))方法解析如果程式碼能執行到這裡說tc需要排隊需要排隊有兩種情況—換言之程式碼能夠執行到這裡有兩種情況:1、tf持有了鎖,並沒有釋放,所以tc來加鎖的時候需要排隊,但這個時候—佇列並沒有初始化2、tn(無所謂哪個執行緒,反正就是一個執行緒)持有了鎖,那麼由於加鎖tn!=tf(tf是屬於第一種情況,我們現在不考慮tf了),所以佇列是一定被初始化了的,tc來加鎖,那麼隊列當中有人在排隊,故而他也去排隊
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方法123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103acquireQueued(addWaiter(Node.exclusive),arg))經過上面的解析之後可以理解成為acquireQueued(nc,arg))
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();}1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162加鎖過程總結:如果是第一個執行緒tf,那麼和佇列無關,執行緒直接持有鎖。並且也不會初始化佇列,如果接下來的執行緒都是交替執行,那麼永遠和AQS佇列無關,都是直接執行緒持有鎖,如果發生了競爭,比如tf持有鎖的過程中T2來lock,那麼這個時候就會初始化AQS,初始化AQS的時候會在佇列的頭部虛擬一個Thread為NULL的Node,因為隊列當中的head永遠是持有鎖的那個node(除了第一次會虛擬一個,其他時候都是持有鎖的那個執行緒鎖封裝的node),現在第一次的時候持有鎖的是tf而tf不在隊列當中所以虛擬了一個node節點,隊列當中的除了head之外的所有的node都在park,當tf釋放鎖之後unpark某個(基本是隊列當中的第二個,為什麼是第二個呢?前面說過head永遠是持有鎖的那個node,當有時候也不會是第二個,比如第二個被cancel之後,至於為什麼會被cancel,不在我們討論範圍之內,cancel的條件很苛刻,基本不會發生)node之後,node被喚醒,假設node是t2,那麼這個時候會首先把t2變成head(sethead),在sethead方法裡面會把t2代表的node設定為head,並且把node的Thread設定為null,為什麼需要設定null?其實原因很簡單,現在t2已經拿到鎖了,node就不要排隊了,那麼node對Thread的引用就沒有意義了。所以佇列的head裡面的Thread永遠為null————————————————版權宣告:本文為CSDN博主「shadow?s」的原創文章,遵循CC 4.0 BY-SA版權協議,轉載請附上原文出處連結及本宣告。原文連結:https://blog.csdn.net/java_lyvee/article/details/98966684