全網最權威:AQS互斥鎖源碼講解(基於ReentrantLock)

AQS 加鎖自旋幾次?排隊的線程修改前一個線程?一般人真不知道。

其實之前在學習 Lock 的時候,學得比較粗糙,我也相信很多人都知道,像 ReentrantLock,ReadWriteLock 都是基於 AQS,CAS 實現的。
通過一個狀態位(或者說標誌位)來 CAS 搶鎖,通過一個 AQS 鏈表隊列,來實現線程的排隊,LockSupport 來實現線程的阻塞與喚醒,通過模板方法設計模式,來對代碼進行封裝。
甚至,可以說基於這些思想,手寫一個簡化版 lock。

確實,如果能理解、學習到這些知識,已經足夠去面試一些中小企業。只不過,相信很多人都有一顆積極向上的心,想要更好的就業平臺,那麼,這些知識還是比較 表層 的。
很多大廠的面試官只要一問,就知道,你是隨便背了幾篇博客,還是真正從 源碼 角度去研究過。這些源碼中的每一處細節,都是寫作者的 思維的結晶,體現出 高度嚴謹、縝密 的編程思想。

學習忌浮躁

下面舉幾個 ReentrantLock 的幾道題目,你來簡單檢測一下自己的學習情況。

  1. 非公平鎖和公平鎖的 底層實現 區別
  2. 線程交替執行(未出現爭搶),執行效率如何
  3. 公平鎖第 2 個線程加鎖,是否會自旋(如果會,自旋幾次)
  4. 隊列 何時 初始化
  5. 隊列 如何 初始化
  6. 持有鎖的線程是否在隊列中
  7. 隊列中保存的 線程狀態 如何記錄
  8. 隊列中首節點存放什麼
  9. 隊列中結點的 waitStatus 是怎樣修改的
  10. lock() 方法爲什麼無法被打斷
  11. 可中斷的 lock 方法是如何做到的
  12. 超時的 lock 方法又做了什麼優化

下面我會將程序的每一步執行的代碼貼出來,先從大的方面,再到小的細節,每一點都寫出詳細的註釋,抽象之處劃出結構圖像幫助理解,來一點一點分析其中的原由。
如果你還不瞭解大體層面,那可以閱讀我之前的文章,先了解有關的基礎知識,這樣從源碼學習纔不至於過於費勁

本文的代碼來自於 JDK 11(可能很多同學都是 1.8 的 JDK,代碼會有差異,但過程、設計思想都是一樣的)

學習完 ReentrantLock,那麼可以去接着學習 ReentrantReadWriteLock,因爲很多知識是相通的,所以我很建議,先從我的這一篇 ReentrantLock 打 AQS 基礎,然後學習 ReentrantReadWriteLock 就會很輕鬆。
共享鎖重入次數怎麼記錄都不知道,誰敢給你漲薪(AQS源碼閱讀之讀寫鎖)

文章目錄

第一個線程加鎖(公平鎖)

調用 AQS 的方法加鎖

首先是調用 sync.acquire() 方法,傳入參數是 1

Sync 是 AQS(AbstractQueuedSynchronizer)的一個子類,然後派生出公平和非公平的 Sync 子類(FairSync、NonfairSync),它們都是 ReentrantLock 的內部類
(這裏可以先不去喜細究,因爲它沒有重寫 AQS 的 acquire 方法)

public void lock() {
    // 調用AQS的acquire方法實現加鎖
    sync.acquire(1);
}

下面進入 AQS 的 acquire 方法
進入 if 判斷,有兩個條件,先調用 tryAcquire() 方法嘗試加鎖
不過這裏是第一個線程加鎖,所以肯定會獲取成功,後面的代碼不會執行

public final void acquire(int arg) {
    // if判斷 有兩個條件
    // 1、先嚐試獲取鎖
    // 不過這裏是第一個線程加鎖,所以肯定會獲取成功、後面的代碼不會執行
    if (!tryAcquire(arg) &&
        // 2、如果獲取不到,則調用下面這行去 排隊
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        // 這裏先不用管下面這行代碼,後文講到打斷時會解釋
        selfInterrupt();
}

嘗試加鎖(此時一定會成功)

所以這時會進入 tryAcquire() 方法中(代碼是由 FairSync(公平鎖) 重寫的)

首先是獲取當前線程和 state 狀態值,如果狀態爲0,表示沒有鎖;大於0,表示加鎖,鎖重入的次數。
如果爲0(未加鎖)

這時是第一次加鎖,所以會進入第一個方法:

  1. !hasQueuedPredecessors()
    是否有線程正在隊列中排隊等待
    (方法前加了 !,所以在隊列沒有線程等待的情況下返回 true,可以向後執行)
  2. 用 CAS 進行加鎖
    加鎖成功後設置持有鎖的線程爲當前線程,然後方法層層返回
    一直到 lock.lock() 方法返回,則可以執行持有鎖的代碼塊了
    如果沒有加鎖成功,當前方法返回false
// 調用公平鎖的tryAcquire()方法獲取鎖
protected final boolean tryAcquire(int acquires) {
    // 獲取當先線程和狀態值
    final Thread current = Thread.currentThread();
    /*
    * 在這裏要說明一下狀態值
    * 在沒有加鎖時爲0,加鎖之後,會變成1,
    * 如果當前線程繼續加鎖,會繼續+1,表示重入次數
    */
    int c = getState();
    // 如果爲0,表示沒有鎖
    if (c == 0) {
        // 兩個判斷,分別執行 
        /*
        * 判斷是否有線程在隊列前面等待
        * 此時第一次來加鎖,所以肯定沒有
        * (再後文詳細描述該方法)
        * 這時的 !tryAcquire 就能返回true
        * 就會進行下一步 CAS 加鎖
        */
        if (!hasQueuedPredecessors() &&
            /*
            * CAS將狀態由0改爲1
            * 
            * 如果成功,則加鎖成功,
            * 則可以將持有鎖的線程改爲當前線程
            * 方法層層返回,一直到lock.lock()然後向下執行
            * 
            * 如果加鎖失敗,這一步返回false
            */
            compareAndSetState(0, acquires)) {
            // 設置持有鎖的線程爲當前線程
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    // else表示不爲0,說明已經加鎖
    // (在後文,第二次加鎖詳細說明)
    else if (current == getExclusiveOwnerThread()) {
        // ... 省略無關代碼
    }
    return false;
}

判斷自己前面有沒有線程排隊 hasQueuedPredecessors

這裏有個小細節
比如有這麼一個問題,一個線程去加鎖,它先看 state 是不是 0,如果是 0,你是不是首先會想到,要用 CAS 加鎖啊。
但是,這裏,又體現出 Doug Lee 大神的功底。對於公平鎖,它不能看到 0 就直接上去加鎖,
它要先看一下隊伍裏有沒有人在排隊,要是有就不行 ,不然就插隊了(可見其縝密)

看源碼發現更是如此:
首先有兩個臨時變量存放 頭結點、第二個結點
爲什麼代碼裏不能直接寫 head,不直接寫 head.next,而要放在另外 兩個變量裏??
你好好想一想,好看??還是閒着沒事??

鎖的作用,肯定是保證多線程的安全。
第一行有一個 head,但是下一行運行的時候,head 還是之前的那個 head 嗎??
會不會運行到後面幾行,head 已經被改成其他節點了?
所以用局部變量裝載它,保證線程安全 !!!!!

public final boolean hasQueuedPredecessors() {
    // 首先要有兩個臨時變量來存一下 頭結點、第二個結點
    // 爲什麼代碼裏不能直接寫 head,不直接寫 head.next,
    // 要把它放在另一個變量裏???
    Node h, s;
    // 如果頭不空
    if ((h = head) != null) {
        // 第二個也不空
        if ((s = h.next) == null || s.waitStatus > 0) {
            // 把s置空,用來裝排在最後面的等待節點
            s = null;
            for (Node p = tail; p != h && p != null; p = p.prev) {
                if (p.waitStatus <= 0) // waitStatus(ws)在後文介紹
                    s = p;
            }
        }
        // 如果有線程在等,且不是自己,則返回true
        if (s != null && s.thread != Thread.currentThread())
            return true;
    }
    return false;
}

CAS

這裏用到了 CAS,大家肯定很熟悉。
首先在目前比較流行的 1.8 版本有 Unsafe 工具類反射獲取可以執行 CAS 操作
以及封裝過的 Automatic 原子類可以執行 CAS

不過筆者這裏用了 JDK11 版本了,在 9 版本就出現了一個類,叫:
VarHandle
這個類可以進行 CAS 操作,在這裏的 ReentrantLock 中的 CAS 也是用的 VarHandle

無競爭加鎖小結

到這裏第一次加鎖就完成了,大家可能覺得到目前爲止這些代碼都很簡單的樣子
確實如此,在沒有線程競爭鎖的情況下,ReentrantLock 的加鎖過程就是:

  1. 判斷是否有線程在隊列中排隊
  2. CAS 加鎖

整個執行過程就是用了一個 CAS 改了一個狀態值來完成加鎖,所以它的 執行效率 很高。
所以 ReentrantLock 的第一個優點就是,在沒有線程競爭的情況下,加鎖效率很高

這也就解答了我在開頭列舉的一個題目:
線程交替執行(未出現爭搶),執行效率如何

第二個線程加鎖(公平鎖)

同樣嘗試加鎖(第一個線程獲取鎖了,所以這裏肯定失敗)

首先進入到嘗試加鎖方法,獲取 state 值
但這是不會是 0 了,是 1,表示有人加鎖了

所以會進入 else 方法塊
判斷當前線程是否爲持有鎖的線程,如果是,state 則加 1
可以看出,這個方法是用來重入的
所以這裏不會進入鎖重入方法,會到最後一行,返回 false,表示嘗試加鎖失敗

protected final boolean tryAcquire(int acquires) {
    // 和第一次一樣
    final Thread current = Thread.currentThread();
    // 獲取 state
    int c = getState();
    if (c == 0) {
        // ... 省略無關代碼
    }
    // 這一次會進入 else 方法塊
    // 然後判斷當前線程是否爲持有鎖的線程
    //(也就是是否是來重入的,這裏是第二個線程來加鎖,顯然不是來重入的)
    else if (current == getExclusiveOwnerThread()) {
        // ... 省略無關代碼
    }
    // 不是來重入的,自然不會運行裏面的代碼,所以返回false
    return false;
}

嘗試加鎖失敗後,開始排隊

這時調用 acquireQueued 方法,判斷是否需要
不過裏面先調用了 addWaiter 方法,將當前線程加入 AQS 等待隊列

// AQS 加鎖方法
public final void acquire(int arg) {
    // 嘗試加鎖
    if (!tryAcquire(arg) &&
        // 這裏第二個線程來,嘗試加鎖失敗
        // 所以來到第二個方法,判斷是否需要排隊
        // addWaiter方法是將線程加入等待隊列
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}

AQS 隊列

必須簡單介紹一下 AQS 的隊列,後文馬上就會出現

因爲 AQS 實現了一個鏈表隊列,所以自然要有鏈表的結點
內容比較多,我這裏只列舉重點 prev、next、thread 這些都不用解釋
waitStatus 等待狀態,簡稱 ws

class Node {
    Thread thread;
    Node prev;
    Node next;
    // 等待狀態(簡稱 ws)
    int waitStatus;
}

node
AQS 隊列有兩個變量 head、tail(首尾結點),一開始沒有初始化時都爲空
aqs

addWaiter 入隊方法(重點細節)

首先創建一個節點
然後進入死循環,我們先看第一次循環

獲取隊尾,判斷是否爲空
此時因爲還沒有人來過隊列,我們從上面一點點分析到現在,還沒看到過隊列有關代碼
所以隊尾一定爲空,
那麼進入 else 方法塊,、、、初始化隊列 !!!!!

private Node addWaiter(Node mode) {
    // 先創建一個節點
    Node node = new Node(mode);
    // 死循環(重點)
    // 第一次循環
    for (;;) {
        // 獲取隊尾
        Node oldTail = tail;
        // 如果不爲空,不過這時還肯定爲空
        // 因爲還沒有人來過隊列
        // 所以到 else
        if (oldTail != null) {
            // ... 省略無關代碼
        } else {
            // 初始化隊列(重點)
            initializeSyncQueue();
        }
    }
}

所以這裏就可以回答前面提到的問題:
隊列 何時 初始化

我們進入初始化的方法
拿到隊首,然後用新建的一個節點用 CAS 放置到隊首,成功後將隊尾也指向這個節點

所以初始化方法就是 new 出一個新節點,然後首尾都指向它

// 初始化隊列方法
 private final void initializeSyncQueue() {
    // 拿到隊頭
    Node h;
    // CAS 創建出一個隊頭,然後隊尾也指向它
    if (HEAD.compareAndSet(this, null, (h = new Node())))
        tail = h;
}

aqs
接下來,初始化完成,我們進入剛纔 addWaiter 入隊方法的第二次循環
這時隊尾就不爲空了,所以隊列就不會再初始化。(所以以後的線程來加鎖也不再會去初始化隊列)
這時死循環裏面做的,就是用 CAS 將隊尾替換爲 新的節點
替換成功就返回這個新節點

private Node addWaiter(Node mode) {
    Node node = new Node(mode);
    // 第二次循環
    for (;;) {
        // 這是獲取隊尾,肯定能獲取到了,不可能再爲空
        Node oldTail = tail;
        if (oldTail != null) {
            // 設置node的前一個結點爲當前的隊尾
            node.setPrevRelaxed(oldTail);
            // CAS設置將 之前的隊尾結點設置爲 新的結點
            //(意思就是將新節點線程安全地插到隊尾)
            // 如果失敗,繼續循環,反覆嘗試,直到將它插到隊尾爲止
            if (compareAndSetTail(oldTail, node)) {
                // 將結點連接
                oldTail.next = node;
                return node;
            }
        } else {
            initializeSyncQueue();
        }
    }
}

在隊列最後CAS加入新節點
在隊列最後CAS加入新節點

重點筆記

這裏我必須重點提一下
我們通過這個入隊方法可以發現:

  1. 在第一次調用入隊方法的時候還沒有隊列,所以會初始化
  2. 初始化的時候就是 new 了一個新節點,這個新節點裏面啥都沒,Thread = null
  3. 這裏我還要提一點,就是 AQS 隊列的隊首永遠是 null。
  4. 從此以後再也不會調用隊列的初始化方法

acquireQueued 在隊列中阻塞方法

這裏傳了兩個參數,一個剛剛入隊的含有當前線程的結點,
還有一個 加鎖時的參數 1。

你好不好奇,這裏爲什麼要把加鎖時的 1 傳過來,我們這裏就排個隊,要這個 1 幹嘛?????
這裏很關鍵

我們看方法

  1. 首先設置一個 false 的 interrupt 的值
  2. 死循環 !!!
  3. 獲取前一個結點
    (我們知道傳過來的結點就是我們這個剛剛加入隊列排隊的線程結點,而隊列剛剛纔初始化,初始化的時候有一個空的結點,所以我們的這個節點就是在這個空節點後面排隊,所以前一個一定是這個空節點,也就是隊頭)
  4. 這時是隊頭,所以 再次 嘗試加鎖 tryAcquire !!!
    (看到沒,知道傳一個加鎖時的 1 有什麼用了吧,因爲這裏還有一次加鎖)
  5. 加鎖失敗 然後進入判斷要不要阻塞的方法

你仔細想一想,這裏是不是很神奇
在之前是先 tryAcquire 方法嘗試加鎖失敗過了,所以它才初始化隊列入隊
按照道理說,是不是它要進了隊列之後睡覺啊,它是不是應該要 park 阻塞住啊
它不阻塞,它不睡覺,它又 tryAcquire 幹嘛

所以說 AQS 的源碼是有很多細節需要我們去思考分析的
你仔細想一想,這是不是就是我們的自旋鎖、
自旋鎖,自旋鎖,平時說的很多,你可不要看到了代碼卻還認不出它
前面嘗試加鎖一次,然後不是直接阻塞,而是又嘗試了一次,這不正是一個自旋了一次的過程 !!!

然後這時候因爲線程 1 還拿着鎖,所以肯定還是失敗
所以不進這個 if,到下一個 if
看到 if 裏面是一個 在加鎖失敗後應不應該睡 的方法
是不是很神奇,它還不睡,還在執行判斷要不要睡的方法

// 這裏傳過來剛纔添加在隊尾的node結點
// 加鎖時的參數 1 也傳了過來
final boolean acquireQueued(final Node node, int arg) {
    boolean interrupted = false;
    try {
        // 又是死循環
        for (;;) {
            // 獲取node的前一個結點
            final Node p = node.predecessor();
            // 判斷是不是頭結點
            // 如果是,再次嘗試加鎖 tryAcquire
            // 我們這裏是第二個線程來,纔剛剛初始化隊列,
            // 纔剛剛添加了一個節點進去,所以肯定是是隊頭結點
            // 所以繼續執行 && 後面的代碼 嘗試加鎖!!
            if (p == head && tryAcquire(arg)) { //這裏加不到鎖,所以不進這個if
                // ... 省略無關代碼
            }
            // 這個方法的名字翻譯過來叫 在嘗試加鎖失敗後應該park
            // 就是一個 應不應該park在嘗試加鎖失敗後 的方法
            if (shouldParkAfterFailedAcquire(p, node))
                interrupted |= parkAndCheckInterrupt();
        }
    } catch (Throwable t) {
        cancelAcquire(node);
        if (interrupted)
            selfInterrupt();
        throw t;
    }
}

我們下面進入這個判斷要不要睡的方法
傳入 前一個結點(prev)和當前結點 後
獲取前一個結點(prev)的 ws 屬性,這個時候肯定是 0
因爲我一行一行代碼寫到這裏,從來就沒沒出現過這麼個 ws 屬性,所以肯定是 node 結點 new 出來的時候的默認初始值 0
所以我們會進最後一個 else,將 ws 用 CAS 改成 -1

// 傳入 前一個結點 和 當前結點
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
    // 獲取前一個結點的 ws 屬性(等待屬性)
    // 在這裏首先我們可以肯定的是,這裏肯定是 0
    // 因爲我寫到這裏,一行一行代碼寫過來,根本就沒出現過這麼個東西
    // 所以只可能是 new 的時候的 默認值 0
    int ws = pred.waitStatus;
    // 如果這個 ws 是 信號-1
    if (ws == Node.SIGNAL) // 這個是 -1
        return true;
    if (ws > 0) {
        // ... 省略無關代碼
    } else {
        // 所以這時一定進這個方法
        // 把上一個結點的 ws 寫成 -1
        pred.compareAndSetWaitStatus(ws, Node.SIGNAL);
    }
    return false;
}

ws = -1
接下來好玩的要來了
剛剛是在死循環裏還記得吧(不然往上再翻一下),現在是第二次循環

獲取前一個結點,看看是不是頭,然後再嘗試加鎖
第三次加鎖了,看到沒 !!!!
所以現在是自旋了 3 次!!!!!!(以後別再老叫它重量級鎖。。)

final boolean acquireQueued(final Node node, int arg) {
    boolean interrupted = false;
    try {
        // 第二次循環來了
        for (;;) {
            // 仍然是獲取前一個結點
            final Node p = node.predecessor();
            // 是隊首,繼續嘗試加鎖
            // 第三次加鎖了吧,記得不??????
            // 當然還是加鎖失敗
            if (p == head && tryAcquire(arg)) {
                // ... 省略無關代碼
            }
            // 又來這個方法 判斷是不是要park
            if (shouldParkAfterFailedAcquire(p, node))
                interrupted |= parkAndCheckInterrupt();
        }
    } catch (Throwable t) {
        // ... 省略無關代碼
    }
}

由於上一次循環,把 ws 改成 -1 了,記得吧
這一次,就會進第一個 if
就會返回 true 了

private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
    int ws = pred.waitStatus;
    // 這一次 ws = -1 了,就返回 true
    if (ws == Node.SIGNAL)
        return true;
    // ... 省略後邊代碼
}

然後就開始 park(睡)。。。

private final boolean parkAndCheckInterrupt() {
    LockSupport.park(this); // 開始睡了
    return Thread.interrupted();
}

看到這裏大家好好想一想,這是個什麼神仙操作??
死循環裏面 tryAcquire 是多嘗試加鎖,來自旋,這個我們知道了

但是又有個 是不是應該睡 的方法
第一次先把 0 改成 -1,下一次循環進來看到是 -1 了,就開始睡
就是爲了多一次循環嗎?這樣就多 tryAcquire 多自旋一次??

還有,爲什麼 t2 要改前一個結點的 ws 爲 -1,爲什麼不是改自己的狀態??
它要阻塞,不是應該把自己的狀態改成阻塞狀態??它去改別人的幹什麼??

留個疑問,我們後文繼續

第二個線程加鎖小結

  1. 在第二個線程加鎖時,由於第一個線程已經加鎖了,所以第一次嘗試加鎖一定失敗
  2. 由於沒有隊列,它要入隊的時候,會初始化一個隊列
  3. 它加入隊列後還有兩次循環,每次循環都會嘗試加鎖一次,也就是額外自旋了 2 次
  4. 這兩次循環,第一次會把前一個結點的 ws(等待狀態)改爲 -1,第二次才阻塞自己

第三個線程加鎖(公平鎖)

同樣先嚐試加鎖 tryAcquire

和第二個線程一樣,肯定是先嚐試加鎖失敗

public final void acquire(int arg) {
    // 先嚐試加鎖 失敗
    if (!tryAcquire(arg) &&
        // 所以會進第二個條件的方法
        // 又是入隊列
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}

addWaiter 入隊

這次入隊列就和上次有那麼一小點不一樣
第二個線程要入隊列的時候,是沒有隊列的,所以第一次循環是初始化隊列,第二次開始纔是 CAS 入隊

而現在第三個線程來入隊,此時已經有隊列存在了,所以直接就是 CAS 入隊操作

// 和上一次的代碼沒什麼區別
private Node addWaiter(Node mode) {
    Node node = new Node(mode);
    // 第一次循環就是 CAS 入隊
    // 死循環 直到CAS入隊成功
    for (;;) {
        Node oldTail = tail;
        if (oldTail != null) {
            node.setPrevRelaxed(oldTail);
            //  CAS將當前線程結點放入隊尾
            if (compareAndSetTail(oldTail, node)) {
                oldTail.next = node;
                // 成功後返回當前結點
                return node;
            }
        } else {
            initializeSyncQueue();
        }
    }
}

t3線程入隊

acquireQueued

這裏和第二個線程又不一樣
我們回顧一下,第二個線程會兩次循環,第一次嘗試加鎖,改前一個結點(頭結點)爲 -1;
第二次再嘗試加鎖,然後睡覺

這時,第三個線程也會先拿到頭結點,但是!!!
前一個結點不是頭結點了,它是排在線程 2 後面的,拿不到開頭的結點了,所以就不會嘗試加鎖

想想,是不是很有道理
第二個線程,因爲接下來輪到的就是它,它可以自旋,多嘗試幾次(如果成功了,就節省了阻塞帶來的開銷)
第三個線程,它排在線程二後面,它要是去嘗試加鎖,那就是插隊,那就不公平了

// 又來到了這個方法
final boolean acquireQueued(final Node node, int arg) {
    boolean interrupted = false;
    try {
        // 死循環
        for (;;) {
            // 獲取前一個結點
            final Node p = node.predecessor();
            // 但是,它不是頭部了
            // 所以不會嘗試加鎖
            // 所以直接到後面的 if
            if (p == head && tryAcquire(arg)) {
                setHead(node);
                p.next = null; // help GC
                return interrupted;
            }
            // 我們記得很清楚,
            // 第一次循環修改上一個結點的 晚上 爲 -1
            // 第二次循環 ws 是 -1 了,所以就阻塞自己
            if (shouldParkAfterFailedAcquire(p, node))
                interrupted |= parkAndCheckInterrupt();
        }
    } catch (Throwable t) {
        cancelAcquire(node);
        if (interrupted)
            selfInterrupt();
        throw t;
    }
}

t3 改 -1

第 3 個線程加鎖小結

  1. 首先由於隊列已經存在,所以不用再初始化隊列了,而是直接進入隊列
  2. 在兩次循環中,由於第一個不是頭結點(也就是在前面還有其他線程在等),所以不會再嘗試加鎖
  3. 兩次循環和之前相同,第一次將前一個結點 ws 改爲 -1,第二次阻塞自己。

非公平鎖加鎖(和公平鎖類似,只是可以插隊)

進方法 tryAcquire

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

開始不一樣了,進了非公平鎖的 tryAcquire 方法
繼續進去

// 內部類 繼承Sync Sync繼承AQS
static final class NonfairSync extends Sync {
    private static final long serialVersionUID = 7316153563782823691L;
    protected final boolean tryAcquire(int acquires) {
        // 進非公平加鎖方法
        return nonfairTryAcquire(acquires);
    }
}

嘗試加鎖方法 nonfairTryAcquire(與公平鎖的差異)

公平鎖和非公平鎖加鎖時唯一不同點(tryAcquire 方法,調用了非公平的 nonfairTryAcquire 方法)
這個方法獲取 當前線程、state 和公平鎖一樣

唯一不同的,就是在嘗試加鎖時:
如果 c==0,就直接 CAS 搶鎖(大家應該還記得公平鎖在嘗試加鎖時要先判斷要不要排隊,隊伍沒人才搶鎖)
而非公平鎖少了這麼一步,只要來了發現鎖沒有人持有,不管隊伍排得多長,有多少人排隊,它都直接 CAS 搶鎖

// 非公平加鎖方法 傳入加鎖參數 1
final boolean nonfairTryAcquire(int acquires) {
    // 前兩行都一樣,獲取 當前線程 和 state值
    final Thread current = Thread.currentThread();
    int c = getState();
    // 如果 c==0
    // 發現不一樣了
    // 公平鎖的時候,要先判斷要不要排隊,再 CAS
    // 這裏直接 CAS(可見其不公平了,可以插隊)
    if (c == 0) {
        // 如果 CAS 成功
        if (compareAndSetState(0, acquires)) {
            // 設置加鎖線程爲自己
            setExclusiveOwnerThread(current);
            return true; // 層層返回 加鎖成功
        }
    }
    // 和公平鎖一樣,是自己就重入 +1
    else if (current == getExclusiveOwnerThread()) {
        int nextc = c + acquires; // 重入次數 + 1
        if (nextc < 0) // overflow // 小於 0 的異常
            throw new Error("Maximum lock count exceeded");
        setState(nextc); // 將重入次數寫到 state
        return true;
    }
    return false;
}

t4

相同點

同樣的,如果是第二個線程來獲取非公平鎖,後面入隊之後也會 額外兩次自旋
只不過自旋的兩次嘗試加鎖的方法是非公平的
不過它由於本來就是排在第一個線程後面,馬上輪到的就該是它,所以加鎖方法公平不公平都是無所謂的

但是再之後有線程來,只會在最開始搶一次鎖,之後的兩次循環在隊伍中同樣不會去嘗試加鎖
這和公平鎖也是相同的

所以,非公平鎖的唯一區別就是在 剛來 lock() 的時候,那一次嘗試加鎖可以爭搶(c==0時不管隊伍有沒有人)。

非公平效率

爲什麼平時都說非公平鎖效率高,主要是因爲:
如果在 t1 去喚醒 t2 的期間,t4 來了,然後很快拿到鎖執行完代碼然後釋放了鎖
t2 才醒過來,那麼原本同樣長的時間之內,多一個線程執行了任務,那麼提高了效率
(線程的阻塞與喚醒是比較耗時的)

線程釋放鎖

首先進 sync 的 release 方法

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

釋放鎖的方法 release

進入到了 釋放鎖的方法,每次釋放鎖傳入 1,用來把 state - 1,減去重入次數,直到 0 完成釋放鎖。

首先用 tryRelease 方法嘗試釋放鎖(因爲可能有重入,再重入情況下一次不會釋放成功)
重入的話就會把 state-1,然後直接 返回 false,解鎖失敗

如果已經解鎖到最後一層了,那麼這一次就會解鎖成功,就要去看我們的隊列
!!!注意 !!!
它是先解鎖,然後去看我們的隊列 !!!
這會有什麼問題???

假如,鎖是非公平的,鎖被釋放了,接下來它纔去隊列裏面叫人。
要是這個時候,如果有其他人來了,就能直接插隊進去加鎖(這個時候隊列裏的人還在呼呼大睡)
它加鎖幹完了事,解鎖走了。
然後隊列裏的那個人才被叫醒,那它開始加鎖開始工作,是不是完全都不知道有人插過隊幹完事又跑了
非公平鎖的效率就是在這裏體現出來的

釋放鎖之後,怎麼知道要不要去隊列裏叫人???
好好想一想。
之前記不記得,後面的線程進隊列要阻塞的時候,做了什麼??
記不記得,把前面的結點,ws 改成了 -1
所以它怎麼知道要不要去叫人??
看一下自己的 ws,如果不是 0 了,說明後面有人了 !!!!

// 傳入 1,用來 減state
public final boolean release(int arg) {
    // tryRelease 嘗試釋放鎖
    // 因爲鎖可能重入過,一次不一定能釋放掉
    // 只有釋放了重入的次數,才能完全釋放鎖
    if (tryRelease(arg)) {
        // 釋放鎖之後,就開始對排隊的隊列操作
        // 注意,鎖是先釋放,再去管隊列裏的人(所以非公平就可能被別人插隊搶到)
        Node h = head; // 找到頭結點
        // 只要不空,並且 ws 不是 0
        // !!現在是不是發現 ws 的作用了
        // 因爲被後面的人改了,所以知道後面有人在排隊,所以纔要去叫醒後面的人
        if (h != null && h.waitStatus != 0)
            // 開始叫醒後面排隊的人
            unparkSuccessor(h);
        return true;
    }
    return false;
}

release

嘗試釋放鎖 tryRelease

總體流程看完了,我們再看釋放鎖的細節
首先獲取 state-1 的值(我們現在不用看代碼都能知道,如果爲 0 就釋放,不爲 0 就不釋放)
如果 c==0 了,就把當前持有鎖的線程置空
最後把 -1 的值寫回 state 裏面
(感覺和前面的相比這裏好簡單)

protected final boolean tryRelease(int releases) {
    // 獲取 state-1 的值
    int c = getState() - releases;
    // 不是加鎖的人來解鎖。。肯定拋異常嘛,不解釋
    if (Thread.currentThread() != getExclusiveOwnerThread())
        throw new IllegalMonitorStateException();
    // free設爲false,目前鎖還不自由
    boolean free = false;
    // 只有等到 c==0 了,說明鎖才自由
    if (c == 0) {
        free = true;
        // 把持有鎖的線程置空
        setExclusiveOwnerThread(null);
    }
    // 把 0 寫進 state 裏,鎖就被完全釋放了
    setState(c);
    return free;
}

喚醒後面的線程 unparkSuccessor(h)

看過之前的代碼(我甚至覺得你完全可以不看代碼,只看我的描述。。),釋放鎖了之後,如果隊列裏有人在排隊等待(阻塞住了),就要去喚醒它

頭結點傳過來,獲取 ws
這時候,由於之前有線程來排隊阻塞了(後一個線程阻塞前把前一個線程 ws 改成 -1),所以 ws 爲 -1;
這時候就會進第一個 if 方法,CAS 把 ws 從 -1 改成 0;

然後開始操作隊列
獲取排在自己後邊那一個結點,只要不是空,就 unpark 叫醒它

// 頭結點被傳了過來
private void unparkSuccessor(Node node) {
    // 如果 ws 小於 0,就CAS改回 0
    // 前面分析過,後一個來排隊的人在阻塞前,會把前一個結點 ws 改成 -1
    int ws = node.waitStatus;
    if (ws < 0)
        // 所以這裏會被改回來
        node.compareAndSetWaitStatus(ws, 0);

    Node s = node.next;
    
    /**
    * ...省略部分代碼
    * 這些其它不正常的情況我在後文統一討論
    * 我們先研究常規情況
    */ 

    // 換醒線程
    if (s != null)
        LockSupport.unpark(s.thread);
}

解鎖小結

解鎖的代碼很簡單,只要你把之前加鎖的過程認真看了,那麼就能很容易理解。

  1. 解鎖要注意的第一個點,就是重入。
    如果重入過了,一次是解不開的。要解多次才能解鎖成功
  2. 還有,就是,解鎖成功之後,纔會去管隊列裏的排隊的線程(而不是,先確認排隊的人叫醒了,再釋放鎖)
    所以體現出的就是非公平鎖的效率。
    (叫人的過程中,可能有其他人來了又走了。這樣,執行次數可能不知不覺就增加了)
  3. 怎麼知道有線程在隊列中阻塞
    後來的線程,阻塞之前,把前一個結點 ws 改成 -1。
    它解鎖的時候發現 ws 不是 0 了,變成 -1 了,說明有人在後面等

線程喚醒

正常喚醒

回顧剛纔的加鎖過程,之後來的線程都會在隊列中阻塞。那麼之後它被喚醒後,又會如何

private final boolean parkAndCheckInterrupt() {
    // 上次運行到這 然後阻塞了
    LockSupport.park(this);
    // 這時被喚醒,可以接着執行了
    return Thread.interrupted();
}

這時 parkAndCheckInterrupt 返回(現在是正常喚醒,不是 interrupt 打斷終止),所以返回 false,繼續來到下一次循環
這時候因爲前一個線程執行完了,將下一個喚醒,所以這時判斷前一個結點是不是頭結點返回的結果一定是 true
所以可以去 tryAcquire 嘗試加鎖
所以一般情況就在這裏加鎖成功,然後 lock() 方法就能繼續往後執行了。

final boolean acquireQueued(final Node node, int arg) {
    boolean interrupted = false;
    try {
        for (;;) {
            // 這時候獲取前一個結點 一定是頭結點了
            final Node p = node.predecessor();
            // 是頭結點然後嘗試加鎖,這時變會加鎖成功
            if (p == head && tryAcquire(arg)) {
                // 把自己設爲頭結點
                setHead(node);
                p.next = null; // help GC
                return interrupted; // false
            }
            if (shouldParkAfterFailedAcquire(p, node))
                // 這時代碼從這裏運行出來了。。然後回到循環開頭。。
                interrupted |= parkAndCheckInterrupt();
        }
    } catch (Throwable t) {
        // ...省略其它無關代碼
    }
}

小細節 setHead

不過這裏有個小細節,我們進 setHead 方法

可以發現:
將自己設置爲頭結點之後,會把 thread 置空

也就是說,AQS 的隊列的頭結點,裏面的 thread 永遠是 null
也就是說,當前持有鎖的線程,不會保存在隊列裏 !!!!

private void setHead(Node node) {
    head = node;
    node.thread = null;
    node.prev = null;
}

release

正常返回就不會調用 selfInterrupt 方法

這時候由於我們的 acquireQueued 方法返回的是 false
(要是不記得了往前翻一點點。。。)

所以不會調用最後一行方法 selfInterrupt
(放心,下面馬上就要說到它了)

public final void acquire(int arg) {
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        // 返回了 false ,所以不會執行 下面的代碼
        selfInterrupt();
}

interrupt 打斷加鎖

lock() 方法 interrupt 結果

首先你知道在執行 lock.lock() 的過程中被 interrupt 打斷會是什麼結果嗎
當調用 lock() 方法阻塞時,如果被別的線程 interrupt,那麼會怎麼樣?

// 其他線程已經持有了鎖
// lock() 阻塞
lock.lock();
// 這裏的代碼在當前線程被別人interrupt後是否會運行?

答案是 lock() 繼續阻塞,不會有反應

interrupt 原理

要理解這些,不光得看源碼,首先你得對 Java 併發編程的基礎掌握。

我們先了解 interrupt 方法。
首先,這個方法一般我們的用途,是某個方法有一個顯示拋出的被 interrupt 打斷的異常(InterruptedException),
然後我們在使用這個方法的時候,用 try catch 捕獲這個異常,然後做出相應的邏輯處理
比如:

try {
    lock.lockInterruptibly();
} catch (InterruptedException e) {
    // 打斷後捕獲異常,執行相應的程序代碼
    System.out.println("我被interrupt了");
    e.printStackTrace();
}

這個是使用層面,在底層原理角度:
interrupt 僅僅只是在線程中添加了一個標記,表示自己被 interrupt 了
這個標記可以被 interrupted 方法捕獲,然後返回 true,同時把標記去除。
(這裏給你看源碼是沒有太大作用的,因爲裏面有 native 方法)

public static void main(String[] args) throws InterruptedException {
    Thread.currentThread().interrupt();
    System.out.println(Thread.interrupted());
    System.out.println(Thread.interrupted());
}

發現:可以捕獲到 打斷標記,同時標記被清除
interrupt

lock() 無法被打斷的原因

之前線程阻塞在 parkAndCheckInterrupt 方法處
裏面用 LockSupport 方法阻塞住了線程

打斷後,返回 Thread.interrupted() 方法 !!!
記得之前我演示的嗎,如果被 interrupt 打斷過,這個方法會捕獲到打斷標記,就會返回 true

private final boolean parkAndCheckInterrupt() {
    LockSupport.park(this); // 阻塞在這
    // 捕獲 interrupt 打斷標記,返回 true
    return Thread.interrupted();
}

過了那麼久,你還記得之前代碼在循環裏面嗎
這時會回到循環的開頭

由於代碼是被 interrupt 打斷後繼續執行的,不是線程釋放鎖將其喚醒,所以這一次循環不會成功加鎖
所以仍舊會循環回到之前那一行代碼,繼續阻塞住。

final boolean acquireQueued(final Node node, int arg) {
    boolean interrupted = false;
    try {
        for (;;) {
            // 因爲循環回到開頭
            final Node p = node.predecessor();
            if (p == head && tryAcquire(arg)) {
                setHead(node);
                p.next = null; // help GC
                return interrupted;
            }
            // 因爲是被打斷醒來,鎖並沒有被其他線程釋放,所以還是回到這裏繼續阻塞
            if (shouldParkAfterFailedAcquire(p, node))
                // 之前線程阻塞在這裏
                // 如果被interrupt(不管是阻塞前還是阻塞後被interrupt,標記都會產生)
                // 所以方法就會 返回true
                // interrupted 變量就等於 true
                // 運算符 |= 保證這個 true標記 不會被循環給覆蓋掉
                interrupted |= parkAndCheckInterrupt();
        }
    } catch (Throwable t) {
        // ...省略其它無關代碼
    }
}

小細節 |= 運算符

這裏又是一個非常有趣的小細節,Doug Lee 大神不是直接 用 = 符號,而是用 |= 去接收變量
想想看,假設用 = 會出現什麼問題?

首先線程被打斷後 返回 true,
然後第二次循環,由於拿不到鎖,在同樣的位置阻塞住,
然後前一個線程釋放鎖,喚醒了它,

這時候注意 !!由於這一次是被正常喚醒的,所以 返回的是 false,就會覆蓋掉前一次被打斷的記錄。
那麼被 interrupt 的記錄就沒了 !!!

interrupted |= parkAndCheckInterrupt();

interrupt 過了,lock() 方法後來被正常喚醒後的結果

我們又來到了這個方法。。。。。
(不是我想講那麼多遍,是因爲情況有那麼多種,我總不能偷工減料,悄悄去掉點內容,讓你們學個半吊子水平)

上一個持有鎖的線程執行完,釋放鎖,開始喚醒當前線程
這時候,我們的線程,被正常喚醒。

這時候 返回的 interrupted() 值一定爲 false
但是,由於之前已經記錄過了 true,所以 |= 之後還是爲 true(保證了沒有被覆蓋掉)

這時候,再一次循環,加鎖就成功,然後就不會再阻塞了,就會返回 true

final boolean acquireQueued(final Node node, int arg) {
    boolean interrupted = false;
    try {
        for (;;) {
            // 又回到了循環開頭
            // (不過沒關係,這一次總算能出去了)
            // (但是非公平鎖可能會被插隊,再多循環幾次。。。。。)(不要緊,總歸快出去了)
            final Node p = node.predecessor();
            if (p == head && tryAcquire(arg)) {
                setHead(node);
                p.next = null; // help GC
                // 加鎖成功後把這個 interrupted=true 返回回去
                return interrupted;
            }
            // 因爲被 interrupt 過了,所我們的 interrupted 變量就位 true
            // |= 運算符保證了 這個true 一直存在
            if (shouldParkAfterFailedAcquire(p, node))
                interrupted |= parkAndCheckInterrupt();
        }
    } catch (Throwable t) {
        // ...省略無關代碼
    }
}

這時候被返回 true 了,我們總算可以執行第三行代碼了

public final void acquire(int arg) {
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        // 前面返回了 true,執行到下一行代碼
        selfInterrupt();
}

不要懵逼,就是把當前線程再打斷一次(把它 interrupt 一下)。。。。
就是調用一下 當前線程的 interrupt() 方法

static void selfInterrupt() {
    Thread.currentThread().interrupt();
}

爲什麼要這麼寫?

看到這裏你可能會覺得很奇怪,這都什麼神仙代碼

爲什麼,它要在之前先調一次 interrupted() 方法,得到了被打斷的結果
然後又在這裏,把之前 interrupted() 抹去的打斷標記再給添加上去

這看起來怎麼這麼無聊?爲什麼要這麼做?
是不是完全可以,那裏就不用判斷什麼是不是被 interrupt 過,這樣就不會抹去標記,這樣就不用再調用 interrupt 方法再把 打斷標記 給添加上去
或者更簡單點,調用 Thread.currenThread.isInterrupted() 方法來判斷是否被打斷,這樣就不會抹去標記,那豈不是更好

更加不可思議的是,他判斷了是否被打斷,但是,判斷了之後,並沒有做任何的事情
那他幹嘛要判斷是否被打斷???

、、
、、
(你要自己好好思考,這些代碼爲什麼這麼設計,如果你想不出來,那很可能就是,你的基礎有欠缺,你的一部分知識沒有掌握,所以不知道里面會出現的問題。
才最終導致你不明白代碼爲什麼這麼寫)
、、
下面我來給大家解釋

首先你要知道,這段代碼本來是阻塞在 LockSupport.park() 那裏的
但是,你去 interrupt 了之後,它就繼續運行了。
也就是說,帶有 interrupt 標記的線程,將無法再被 park() 阻塞 !!!!
示例:

public static void main(String[] args) throws InterruptedException {
    Thread t = new Thread(() -> {
        for(;;) {
            // 死循環 每一次都會park自己
            System.out.println("我將park自己");
            LockSupport.park(Thread.currentThread());
            System.out.println("我醒了");
        }
    });
    t.start();
    // 2秒後打斷 線程t
    Thread.sleep(2000);
    t.interrupt();
}

開始,線程 t 打印了 “我將park自己”,然後就沒反應了(因爲它把自己 park 住了)
test
然後,interrupt 它。
然後它就懵逼了,它每一次循環,都要 park 自己。
但是它 park 不住啊,它死活 停不下了,它帶了一個 interrupt 標記,它停不下來,就會瘋狂循環
test
再看這裏
(又看到這段代碼了,所以說,這段代碼很重要)

在 parkAndCheckInterrupt 方法這裏,本來應該 park 阻塞起來,但是 !!!
停不下來啊,怕是嚼了炫邁
然後就瘋狂循環。。
你想,要是幾百個線程不做事,在這裏瘋狂循環,你這機子還怎麼做事情 !!!

所以,他先獲取 interrupt 標記,把它抹去,是爲了讓它繼續阻塞起來。最後搶到鎖了,那就可以把標記還給它了。

final boolean acquireQueued(final Node node, int arg) {
    boolean interrupted = false;
    try {
        for (;;) {
            final Node p = node.predecessor();
            if (p == head && tryAcquire(arg)) {
                setHead(node);
                p.next = null;
                return interrupted;
            }
            if (shouldParkAfterFailedAcquire(p, node))
                // 本來要阻塞在裏面的,但是,阻塞不了了
                // 然後就會瘋狂循環
                // 你想,要是幾百個線程不做事,在這裏瘋狂循環,你這機子不是分分鐘掛掉
                interrupted |= parkAndCheckInterrupt();
        }
    } catch (Throwable t) {
        // ...省略部分無關代碼
    }
}

lock() 被 interrupt 小結

  1. 首先,在阻塞之中,如果被 interrupt,自然會醒過來
  2. 但是,這個時候屬於非正常喚醒(前面線程還沒釋放鎖,它醒過來就會自旋瘋狂加鎖,浪費 CPU),所以先獲取並抹去這個線程的 interrupt 標記,讓它在下一個循環過後繼續阻塞。
  3. 用 |= 運算符保證了 標記一但被獲取就無法覆蓋
  4. 最後線程正常喚醒拿到鎖之後,用 interrupt() 方法重新給它再添加回標記

lockInterruptibly 可打斷的 lock 方法

理解了上面的 lock 方法,以及爲什麼無法被打斷,那麼接下來你要理解 lockInterruptibly 是如何被打斷的就會非常輕鬆了
因爲 lockInterruptibly 的方法和傳統 lock 方法並沒有太大的差異,只是在幾個地方,在 interrupted() 獲取到 interrupt 標記之後,拋出 InterruptedException 異常讓用戶定義處理邏輯而已

我們開始點進方法中
發現調用了 sync 的 acquireInterruptibly(1) 方法
(見名知意,其實學習到現在,應該不用看源碼也能大致知道實現過程了)

public void lockInterruptibly() throws InterruptedException {
    sync.acquireInterruptibly(1);
}

acquireInterruptibly 加鎖前打斷

我們繼續點進方法
發現:

在加鎖之前,先判斷是否被打斷,如果被打斷了,就不用加鎖了,直接拋異常

要是這時沒被打斷,就開始執行後面的加鎖代碼

public final void acquireInterruptibly(int arg)
        throws InterruptedException {
    // 這時候還沒開始加鎖
    // 在加鎖前就判斷是否被打斷,如果被打斷了,直接拋異常
    if (Thread.interrupted())
        throw new InterruptedException();
    // 然後就是加鎖方法了
    if (!tryAcquire(arg)) // 熟悉的tryAcquire(這裏就可以不用看了,前面已經學地很紮實了)
        // 我們點進下一行方法
        doAcquireInterruptibly(arg);
}

眼熟的 doAcquireInterruptibly

點進來之後,怎麼看怎麼不對勁
這代碼怎麼那麼眼熟??

說白了,就是 lock 方法稍微改一改,整體思路就沒變
所以學會了前面的基礎,這裏我都不用帶你們看,你們也應該能自己看懂了

和 lock 方法相比,也就最後 parkAndCheckInterrupt 阻塞的時候改了一下
不是返回是否被打斷的 布爾值,而是打斷直接拋異常

private void doAcquireInterruptibly(int arg)
    throws InterruptedException {
    // 熟悉的 addWaiter 方法(不記得回到上面複習)
    final Node node = addWaiter(Node.EXCLUSIVE);
    try {
        // 熟悉的 for 循環
        for (;;) {
            // 熟悉的加鎖過程
            final Node p = node.predecessor();
            if (p == head && tryAcquire(arg)) {
                setHead(node);
                p.next = null; // help GC
                return;
            }
            // 熟悉的阻塞過程
            // 誒? 等等,這裏不一樣了
            // 這裏不返回是否狀態了,如果被打斷,直接拋異常
            // 這麼多代碼,也就這裏不一樣。。。
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                throw new InterruptedException();
        }
    } catch (Throwable t) {
        cancelAcquire(node);
        throw t;
    }
}

catch 異常

你不會以爲就只有這麼簡單吧,其實還沒有結束。。
(其實我在一開始看源碼的時候,以爲這樣已經結束了,但後來一想才發現不對)
(你要是也這麼想,那還是老老實實繼續跟着我往下分析)

不知道你有沒有注意到一個問題,在 doAcquireInterruptibly 的方法裏,我們的循環方法,是被 try catch 給包起來的,所以,
拋出的異常會被捕獲。

這裏你可能很頭疼,我們的 interrupt 中斷,就是爲了讓它拋異常,我們才能自己手動捕獲,去處理中斷邏輯。
但是它自己把異常捕獲了,是不是很神奇??
想想看,爲什麼他要這麼寫??
現在我們就要好好分析一下。

本來,我們的線程是在 AQS 隊列中排隊的,
如果,它突然被打斷了,我們直接捕獲了異常,去處理相應邏輯,
但是,這個線程還在隊列裏啊,它還在裏面排隊啊,這個時候,它已經被打斷做自己的事了,
等到上一個線程執行完了,然後回來叫它,它會有反應嗎?
顯然沒有,所以,這樣我們的 AQS 隊列就掛了,後面的線程會永遠在排隊,再也不會被叫醒了。

這樣的話,我們的 Doug Lee 就會被人吐槽,這什麼垃圾鎖,老是一打斷就死鎖。

所以,在這裏用 catch 先捕獲了 這個異常,把這個被打斷的線程,在隊列裏處理一下,保證隊伍健在
然後再把這個異常重新拋出去,給我們的用戶
但是 !!!!!
在這個 catch 異常之後的處理邏輯非常複雜,我在後文要花很多時間去分析
catch

lockInterruptibly 小結

你只要跟着我一行一行代碼看過來,你就會發現,實際上這裏就改了兩個地方。

  1. 加鎖前先判斷是否被打斷
    打斷直接拋異常
  2. 要是加鎖前沒有,就開始加鎖。
    要是加鎖沒成功,執行到了 park 阻塞的那個地方,要是被打斷了,直接拋異常
  3. 拋出異常後,自己先 catch,然後調整隊列,再將異常重新拋出

失效結點的調整

調整的思路,應該是要讓前面的線程能夠去通知後面的線程
但是,由於多線程的情況下,不加鎖的情況下,沒有原子操作能夠將其完好的踢出去,所以得有一個非常複雜的分析,存在很多情況去處理。
cancelAcquire

情況過於複雜(畫圖分不同情況探討)

  1. 首先會把自己的 thread 變量設置爲空
  2. 它會先一個一個往自己前面找,找到第一個可用的結點(沒失效的)讓自己指向它,作爲自己的前置結點
  3. 然後把 自己的 ws 改成 1,說明自己沒用了(失效了)
    (把自己剔除隊列有很多過程,防止自己還沒有出隊,其他線程就來幹各種事情,所以先把自己標記起來)
  4. 然後,如果自己後面沒有結點,那麼直接 CAS 把自己移除隊列
  5. 如果後面還有結點,並且前置結點不是頭結點 、並且沒有失效,那就要先往後找,找到第一個有效結點爲止,然後把自己前面的結點和後面的結點連接起來
  6. 如果自己前面的就是頭結點,或者前置結點已經失效,就去喚醒後面的線程,然後將自己的後置結點設爲自己(就是不能從自己這裏再往後面找結點了)
  7. 如果後面的線程被叫醒了,就會往前去尋找到可用結點,把它指向自己(能夠從前往後連起來)

先把 thread 置空

node.thread = null;

找未失效的前置結點

// 這裏就是不斷往前尋找,找到一個 ws 不大於 0 的結點做前一個結點
// 我們之前看到的,都是 ws 等於 0 或者 -1
// -1 表示後面有人排隊阻塞,
// 這裏的 大於0 是指 1,是指那些被打斷了的、已經沒用的,還在隊列裏的結點、
// 這時候把自己的前置結點設置爲找到的這個節點
Node pred = node.prev;
while (pred.waitStatus > 0)
    node.prev = pred = pred.prev;
// 這是個小細節
// 它只把自己的前一個結點設置成找到的第一個有效結點
// 但是不把這個節點的後結點改成自己
// 只是獲取了它的後置結點
Node predNext = pred.next;

突然發現我的 4、5 兩個結點忘記連起來了。。。。。
(畫太多了,眼睛不好了)
cancelAcquire

把自己 ws 改成 1(說明自己失效)

node.waitStatus = Node.CANCELLED;

如果自己是最後一個,就 CAS 出隊

看起來如果是在最後一個,操作是很簡單的

if (node == tail && compareAndSetTail(node, pred)) {
    pred.compareAndSetNext(predNext, null);
} 

出隊

後面有結點,前面不是頭結點且沒失效

// 如果後面還有人(這時要把自己移除就比較複雜)
// 先看前一個是不是頭結點,如果不是,並且 ws=-1(就算不是也給它改成-1)
// (上面的操作已經保證了它 ws 不可能 >0,它是個可用結點)
// 然後下面的方法 會把自己前面和後面連接起來,那自己就不在隊列內了
// (但如果後面那個恰巧不可用,那就不連接,於是剔除自己失敗)
int ws;
if (pred != head && // 不是頭部
    ((ws = pred.waitStatus) == Node.SIGNAL ||   // ws = -1
    // 如果可用就改成 -1
     (ws <= 0 && pred.compareAndSetWaitStatus(ws, Node.SIGNAL))) &&
    pred.thread != null) { // 不是頭,又不是不可用的,所以thread肯定也不空
    Node next = node.next; // 找到下一個結點
    // 如果下一個結點不空,並且 ws<=0(說明可用)
    // 就把前一個結點CAS指向後一個(這樣就把前後連接起來)
    if (next != null && next.waitStatus <= 0)
        pred.compareAndSetNext(predNext, next);
} 

cancelAcquire

前置爲頭,或者突然失效

else {
    // 進入else方法(說明前一個是頭結點、或者突然失效了)
    // 爲什麼頭結就不能夠直接把前後連起來???(好好思考)
    // 因爲下一個線程就是自己,但是又不知道它什麼時候會釋放鎖去喚醒線程。
    // 很可能這個時候還沒有連接前後,但是上一個線程(頭)已經釋放了鎖,並且喚醒當前線程(失效線程)
    // 這樣,等你連接好了前後,但是前面的頭結點已經不會再去喚醒下一個線程了,下一個線程就永遠阻塞
    // 所以,這個方法就是去叫醒後面的線程
    // (頭結點只是一種情況,假設前置結點在多線程環境下突然失效了,就不能連接失效結點)
    // 而喚醒後面的線程,它發現前一個結點失效了,它會自己去找前面的結點連接
    unparkSuccessor(node);
}

前置爲頭結點

假設在改引用前,t1 去喚醒 t2(這一步是沒有任何作用的),
然後,t2 才把 頭結點 的引用改成 t3,
這時,雖然 頭結點 的引用被正確指向了 t3,但是已經晚了,t1 已經去喚醒過了,不會重新去喚醒 t3

所以,如果 前置結點 是頭結點,就不去改引用,而是去喚醒後面的結點
head

前置節點突然失效

如果前置結點突然失效,那麼此時就算修改了前置結點的引用指向了後面的結點,
拿一個失效的結點去指向它,仍然沒有用處
pred

喚醒後面的有效線程

// 調用這個方法會把自己這個節點作爲參數傳進來
private void unparkSuccessor(Node node) {
    // 獲取自己的 ws,如果 <0 就改成 0
    // ws <0 表示後邊有線程在等待,所以自己的狀態纔會 <0
    // 這時後將 ws 改爲 0,然後後面調用 unpark 方法去喚醒它
    // (一般在這裏 ws 都是 1,因爲失效才搞了一大堆事情)
    int ws = node.waitStatus;
    if (ws < 0)
        node.compareAndSetWaitStatus(ws, 0);

    // 從最後往前找,直到找不到了或者找到了自己爲止,
    // 找出最前面的 有效節點
    Node s = node.next;
    if (s == null || s.waitStatus > 0) {
        s = null;
        for (Node p = tail; p != node && p != null; p = p.prev)
            if (p.waitStatus <= 0)
                s = p;
    }
    // 只要不是空,就將它喚醒
    if (s != null)
        LockSupport.unpark(s.thread);
}

喚醒結點

被喚醒的有效線程自己去尋找前面的有效結點

現在又回到你們熟悉的代碼段了,仔細看
記得當時這個 判斷應不應該阻塞 的方法,它被執行了兩次,第一次把 前一個結點的 ws 從 0 改成 -1,第二次 發現是 -1 了,這個時候就阻塞自己

現在我們的這個線程來到這個方法,發現前一個結點是 1(一個失效結點)
這時候它就會再繼續往前不斷地找,直到找到一個有效結點爲止
讓這個有效結點指向自己

// 還記得這個判斷要不要排毒的方法嗎
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
    int ws = pred.waitStatus;
    if (ws == Node.SIGNAL)
        return true;
    
    // 我們只要關注這裏
    if (ws > 0) {
        // 當剛纔的失效結點喚醒了後面的這個節點之後,就會用一個循環
        // 只要 ws > 0,就不斷往前找
        // 這樣最終h會找到一個 有效結點
        do {
            node.prev = pred = pred.prev;
        } while (pred.waitStatus > 0);
        // 然後將那個有效結點的後置結點設爲自己
        // 這樣,隊伍從前往後就是連起來了(可以從前往後找)
        pred.next = node;
    
    } else {
        pred.compareAndSetWaitStatus(ws, Node.SIGNAL);
    }
    return false;
}

find

下面整體看一下這些代碼

cancelAcquire 試圖將自己剔除出隊列

主要目的是把自己從隊列裏踢出去(因爲自己被打斷了,不用再排隊上鎖了)
這裏有一個 ws 值:CANCELLED 爲 1,表示已經失效了(相當於隊列裏不存在它了,已經沒用了)

  1. 首先會把自己的 thread 變量設置爲空
  2. 它會先一個一個往自己前面找,找到第一個可用的結點(沒失效的)讓自己指向它,作爲自己的前置結點
  3. 然後把 自己的 ws 改成 1,說明自己沒用了(失效了)
    (把自己剔除隊列有很多過程,防止自己還沒有出隊,其他線程就來幹各種事情,所以先把自己標記起來)
  4. 然後,如果自己後面沒有結點,那麼直接 CAS 把自己移除隊列
  5. 如果後面還有結點,並且前置結點不是頭結點 、並且沒有失效,那就要先往後找,找到第一個有效結點爲止,然後把自己前面的結點和後面的結點連接起來
  6. 如果自己前面的就是頭結點,或者前置結點已經失效,就去喚醒後面的線程,然後將自己的後置結點設爲自己(就是不能從自己這裏再往後面找結點了)
// 傳入當前線程結點
private void cancelAcquire(Node node) {
    // 如果空,說明自己不在隊列裏,就不會影響到隊列,就不必做其他操作
    // (我目前的分析是不會出現這種情況,加這一行雖然可能多餘,但萬一有也可以保證安全不出錯)
    // (如果大家有找到這種情況可以給我留言)
    if (node == null)
        return;
    
    // 首先把自己 thread變量 置空
    // (一開始我以爲是爲了讓這個節點以後成爲頭結點的時候 thread 要爲空)
    // (不過把整體分析了之後,這個節點不會有成爲頭結點的機會,應該只是爲了 help gc)
    node.thread = null;

    // 這裏就是不斷往前尋找,找到一個 ws 不大於 0 的結點做前一個結點
    // 我們之前看到的,都是 ws 等於 0 或者 -1
    // -1 表示後面有人排隊阻塞,
    // 這裏的 大於0 是指 1,是指那些被打斷了的、已經沒用的,還在隊列裏的結點、
    // 這時候把自己的前置結點設置爲找到的這個節點
    Node pred = node.prev;
    while (pred.waitStatus > 0)
        node.prev = pred = pred.prev;
    // 這是個小細節
    // 它只把自己的前一個結點設置成找到的第一個有效結點
    // 但是不把這個節點的後結點改成自己
    // 只是獲取了它的後置結點
    Node predNext = pred.next;

    // 把自己的 ws 寫爲 1
    // 看到這,也就很容易理解上面的操作
    // 1 表示自己已經被打斷了(不再有效)不用在隊列裏理會我
    // 就像上一步操作,直接跳過 1 的結點,往前找不是 1 的結點
    node.waitStatus = Node.CANCELLED;

    // 之前的操作是將前面的無效結點剔除,重新連接起一條隊列
    // 這裏設置自己無效,可以讓自己前面、後面的結點能知道要跳過自己
    // 要是自己就在尾部,後面沒有結點了,那就可以很簡單,CAS直接把自己移除
    if (node == tail && compareAndSetTail(node, pred)) {
        pred.compareAndSetNext(predNext, null);
    } else {
        // 如果後面還有人(這時要把自己移除就比較複雜)
        // 先看前一個是不是頭結點,如果不是,並且 ws=-1(就算不是也給它改成-1)
        // (上面的操作已經保證了它 ws 不可能 >0,它是個可用結點)
        // 然後下面的方法 會把自己前面和後面連接起來,那自己就不在隊列內了
        // (但如果後面那個恰巧不可用,那就不連接,於是剔除自己失敗)
        int ws;
        if (pred != head && // 不是頭部
            ((ws = pred.waitStatus) == Node.SIGNAL ||   // ws = -1
            // 如果可用就改成 -1
             (ws <= 0 && pred.compareAndSetWaitStatus(ws, Node.SIGNAL))) &&
            pred.thread != null) { // 不是頭,又不是不可用的,所以thread肯定也不空
            Node next = node.next; // 找到下一個結點
            // 如果下一個結點不空,並且 ws<=0(說明可用)
            // 就把前一個結點CAS指向後一個(這樣就把前後連接起來)
            if (next != null && next.waitStatus <= 0)
                pred.compareAndSetNext(predNext, next);
        } else {
            // 進入else方法(說明前一個是頭結點、或者突然失效了)
            // 爲什麼頭結就不能夠直接把前後連起來???(好好思考)
            // 因爲下一個線程就是自己,但是又不知道它什麼時候會釋放鎖去喚醒線程。
            // 很可能這個時候還沒有連接前後,但是上一個線程(頭)已經釋放了鎖,並且喚醒當前線程(失效線程)
            // 這樣,等你連接好了前後,但是前面的頭結點已經不會再去喚醒下一個線程了,下一個線程就永遠阻塞
            // 所以,這個方法就是去叫醒後面的線程
            // (頭結點只是一種情況,假設前置結點在多線程環境下突然失效了,就不能連接失效結點)
            // 而喚醒後面的線程,它發現前一個結點失效了,它會自己去找前面的結點連接
            unparkSuccessor(node);
        }
        // 如果出隊成功了,這時才能如同作者所說的 help gc
        // 這一步是非關鍵的,只有前面的操作讓隊列中將自己踢出了,纔行
        node.next = node; // help GC
    }
}

unparkSuccessor 喚醒後面的線程

// 把自己這個節點傳過來
private void unparkSuccessor(Node node) {
    // 獲取自己的 ws,如果 <0 就改成 0
    // ws <0 表示後邊有線程在等待,所以自己的狀態纔會 <0
    // 這時後將 ws 改爲 0,然後後面調用 unpark 方法去喚醒它
    int ws = node.waitStatus;
    if (ws < 0)
        node.compareAndSetWaitStatus(ws, 0);

    // 從最後往前找,直到找不到了或者找到了自己爲止,
    // 找出最前面的 有效節點
    Node s = node.next;
    if (s == null || s.waitStatus > 0) {
        s = null;
        for (Node p = tail; p != node && p != null; p = p.prev)
            if (p.waitStatus <= 0)
                s = p;
    }
    // 只要不是空,就將它喚醒
    if (s != null)
        LockSupport.unpark(s.thread);
}

shouldParkAfterFailedAcquire 被喚醒的線程自己去向前找有效結點

還記得那個判斷自己要不要排隊的方法嗎??(畢竟隔了很久)
當時我們只是研究了 ws=0 和 ws<0 的情況,用於做兩次自旋

而這時就是 > 0 的情況了(=1)
所以我們這裏只需要看 大於0 的那段代碼

這時,這個線程就是被前面的那個失效的線程喚醒的後面的結點裏的線程(前面的失效結點,發現條件不滿足,自己不能夠去連接隊列,所以在 else 中喚醒後面的線程),
它要自己去找前面的有效結點做連接,
這個代碼就是爲了將前面的有效結點,連到後面的有效結點(就是剛剛被喚醒的自己)來

// 還記得這個判斷要不要排毒的方法嗎
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
    int ws = pred.waitStatus;
    if (ws == Node.SIGNAL)
        return true;
    
    // 我們只要關注這裏
    if (ws > 0) {
        // 當剛纔的失效結點喚醒了後面的這個節點之後,就會用一個循環
        // 只要 ws > 0,就不斷往前找
        // 這樣最終h會找到一個 有效結點
        do {
            node.prev = pred = pred.prev;
        } while (pred.waitStatus > 0);
        // 然後將那個有效結點的後置結點設爲自己
        // 這樣,隊伍從前往後就是連起來了(可以從前往後找)
        pred.next = node;
    
    } else {
        pred.compareAndSetWaitStatus(ws, Node.SIGNAL);
    }
    return false;
}

帶超時的 tryLock

其實學完了上面的內容之後,這一部分我完全可以不用提點,你們自己也該能看的懂
因爲這個方法中的關鍵代碼,調用的也都是我之前已經帶你們分析過的方法了

public boolean tryLock(long timeout, TimeUnit unit)
        throws InterruptedException {
    // 帶超時的加鎖
    return sync.tryAcquireNanos(1, unit.toNanos(timeout));
}

似曾相識的方法

我們點進方法看一下
是不是很熟悉?
和 lockInterruptedly 方法 在這裏沒有什麼差別

同樣的,是先在加鎖前判斷是否被打斷,如果是,就不用加鎖,直接拋出異常
然後,嘗試加鎖,
如果沒有成功加鎖,進入超時加鎖方法

public final boolean tryAcquireNanos(int arg, long nanosTimeout)
        throws InterruptedException {
    // 如果加鎖前被打斷,直接拋異常
    if (Thread.interrupted())
        throw new InterruptedException();
    // 否則,嘗試加鎖一次
    return tryAcquire(arg) ||
        // 嘗試加鎖失敗就進入超時加鎖方法
        doAcquireNanos(arg, nanosTimeout);
}

是不是總感覺這一段一段代碼都和之前的非常相似

這裏面和之前不同的是:

多了一個判斷時間的地方
如果超時了,就用 之前打斷後調整隊列的方式,讓自己失效

如果沒超時,並且剩餘時間還大於 1秒,就阻塞到時間結束
如果時間不夠 1秒,就不阻塞了,直接自旋 !!!

private boolean doAcquireNanos(int arg, long nanosTimeout)
        throws InterruptedException {
    // 在入隊前判斷是否超時
    // (這裏用戶定的時間 <= 0,那就相當於不允許等待,tryLock 沒成功就直接返回 false)
    if (nanosTimeout <= 0L)
        return false;
    // 計算剩餘時間
    final long deadline = System.nanoTime() + nanosTimeout;
    // 如出一轍的入隊操作(不記得了回到上面複習)
    final Node node = addWaiter(Node.EXCLUSIVE);
    try {
        // 熟悉的 for 循環
        for (;;) {
            // 熟悉的前面是隊頭就嘗試加鎖
            final Node p = node.predecessor();
            if (p == head && tryAcquire(arg)) {
                setHead(node);
                p.next = null; // help GC
                return true;
            }
            
            // 需要注意的是
            // 多了一層時間的判斷
            // 如果等待時間達到了,就調用之前講過的 cancelAcquire 方法
            // 調整隊列,讓自己失效
            nanosTimeout = deadline - System.nanoTime();
            if (nanosTimeout <= 0L) {
                cancelAcquire(node);
                return false;
            }
            // 這裏也需要注意
            // 不僅僅是前面的結點 改成-1 就直接 park
            // 你看這個 if 判斷 後面還有一個 剩餘時間要 大於 1秒
            if (shouldParkAfterFailedAcquire(p, node) &&
                nanosTimeout > SPIN_FOR_TIMEOUT_THRESHOLD)//這個常量是1秒
                // 然後才允許阻塞到剩餘時間結束
                // 也就是如果時間不到一秒鐘 是不讓阻塞的
                // 這個時候就是最長自旋 1秒 的自旋鎖,不阻塞!!!!
                LockSupport.parkNanos(this, nanosTimeout);
            // 如果被中斷了 就throw異常 讓catch處理後 再拋出
            if (Thread.interrupted())
                throw new InterruptedException();
        }
    } catch (Throwable t) {
        cancelAcquire(node);
        throw t;
    }
}

帶超時的 lock 小結

  1. 首先和 可以被打斷的 lock 一樣,在加鎖前先判斷是否被打斷,如果被打斷了,就不用加鎖,直接拋出異常讓用戶處理打斷邏輯
    (因爲待超時的 lock 也是一種可以被打斷的 lock)
  2. 然後同樣先嚐試加鎖一次
  3. 加鎖不成功的話,先判斷時間有多少,如果給的時間 本來就不大於 0,那就直接返回 false 表示加鎖失敗
  4. 否則就入隊
  5. 入隊後進入熟悉的循環,每一次循環都會判斷是否前一個是隊頭要去嘗試加鎖
  6. 循環中如果時間過了,就可以 返回 false 加鎖失敗了,但是返回前要先老樣子調整隊列,讓自己失效
  7. 如果循環中時間還沒過,那就看一下當前是否可以阻塞,並且 如果剩餘時間 > 1秒 才阻塞,否則在循環中自旋
  8. 如果被打斷了,老樣子,catch 捕獲,處理了隊列之後,再重新拋給用戶

作者的話

到這裏,整個 ReentrantLock 的加解鎖你應該已經理解得十分透徹了。
(畢竟我是一行一行給你讀過來的)

畢竟現如今網絡上的博客,大部分是比較 簡單 的,我並沒有找到能把這些加鎖解鎖的過程從 代碼 層面寫清楚的人,而且裏面很多 關鍵的點,這些 重要的思想,很少有去 分析 的。

我們閱讀源碼,一方面,是瞭解它的實現過程。
但是還有一方面,我們是要學習作者的 優秀的編碼思想,和源碼中每一處這麼寫的 原由

不過由於源碼 博大精深,我也只是 略有窺探,想來很多地方我也許是有 遺漏 的。所以,如果讀者發現什麼不全、或者錯誤的地方,也 懇請指正

如果你僅僅只是把它讀了一遍,但是不知道,爲什麼這個地方要寫這樣一段代碼,爲什麼要寫這麼多看似無關的複雜的邏輯。那我認爲,你從源碼學習到的也只是一些 皮毛
就比如我這篇文章中寫到的,先獲取抹去 interrupt 標記,再給它添加上;當一個線程自己被打斷失效了,卻又去喚醒了後面的線程;進入 tryAcquire 方法後,如果沒線程持有鎖,而是先判斷要不要排隊……
等等等等,有很多很多需要 思考分析 的地方。

這篇博客我寫了兩天多,大概二十多個小時,我是從源碼中,一個方法一個方法進去,去閱讀裏面的代碼,然後複製過來,寫上我自己的註釋,以及對這些代碼的 分析思考,去研究 爲什麼 作者要這麼去寫,否則 是否會出現錯誤

如果你只是和閱讀其他博客一樣,花個幾分鐘,或者,我給你多算一點時間,花個半或一小時,來閱讀。那麼我覺得,你實際上仍是 一知半解
其實,學習源碼的最好方式,就是從頭開始,一行一行代碼去往裏面分析,先閱讀當時執行相關的代碼,忽略其他那些無關代碼。
就比如我先用第一個線程加鎖,就可以忽略 AQS 的隊列(單線程執行不會初始化隊列);然後第二個線程加鎖,就要去閱讀隊列有關的代碼,然後瞭解 ws 的有關的狀態值;在後面研究 interrupt 打斷失效的時候,才研究 ws 的失效狀態值的含義。

當你能把我的文章裏的內容看明白,不管你學習得有多深入了,至少可以保證,裏面的大致流程你已經清楚,在這樣的情況下,如果你再去閱讀、研究源碼,在我給你的基礎之上,你可以比較輕鬆的理解出源碼中的代碼。
假設你從來沒有接觸過,直接扎進去閱讀,而沒有一個大體概覽的話,那閱讀便如同天書一般。

(允許我小調皮一下,研究那麼久那麼多我也是很頭大)
當然大部分人應該不會看到我的這一行,如果你覺得我這篇博客能真正給你帶來益處,就 素質三連。。。

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