ReentrantLock加鎖源碼淺析

加鎖邏輯將分成三個部分來看:

  • 競爭鎖
  • 加入等待隊列
  • 阻塞等待

1.競爭鎖
我們先從公平鎖入手

public void lock() {
    // sync的實例是new FairSync()
    sync.acquire(1);
}
// 加鎖的代碼就是這幾行
public final void acquire(int arg) {
    if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}

上述代碼可以拆分成以下幾段:

// 競爭鎖
tryAcquire(arg)
// 加入等待隊列
addWaiter(Node.EXCLUSIVE)
// 阻塞等待
acquireQueued(node, arg)
  • 競爭鎖
protected final boolean tryAcquire(int acquires) {
    // 獲取當前線程
    final Thread current = Thread.currentThread();
    // 獲取當前state狀態
    int c = getState();
    // 如果當前state是沒有任何線程搶占的話
    if (c == 0) {
        // 如果等待隊列中有任何一個等待的節點,都不會搶占鎖
        if (!hasQueuedPredecessors() &&
            // CAS搶占鎖成功
            compareAndSetState(0, acquires)) {
            // 搶占成功后,標記當前線程已經搶占到鎖了。
            setExclusiveOwnerThread(current);
            // 返回加鎖成功
            return true;
        }
    }
    // 如果是同一個線程重復加鎖的情況下
    else if (current == getExclusiveOwnerThread()) {
        // 在這種情況下,只是簡單地操作state
        int nextc = c + acquires;
        if (nextc < 0)
            throw new Error("Maximum lock count exceeded");
        // 因為當前線程已經加鎖成功了,再次加鎖的話,直接在state上增加加鎖次數即可。
        setState(nextc);
        // 返回加鎖成功
        return true;
    }
    // 如果已經有別的線程加鎖了,或者還有很多線程在排隊等待,那么返回false加鎖失敗。
    return false;
}

上述代碼分幾部分:

  1. 如果當前state=0,也就是沒有任何線程搶占鎖的情況下
    1.1: 沒有等待隊列的情況下,可以CAS搶占鎖
    1.2: 有等待隊列的話,該隊列中第一個等待節點不是當前線程,不可以搶占鎖,因為這是公平鎖。


    image.png

    如果當前等待隊列中還有任意節點,并且當前節點中的線程不是當前線程,說明有其他線程處于等待過程中,那么當前線程就應該乖乖排隊去。

1.3: 有等待隊列,并且當前第一個等待節點就是當前線程,可以搶占鎖。這種情況會出現在線程剛從阻塞中被喚醒的時候。
image.png

假如當前線程是被剛剛喚醒的,并且它處于等待隊列中的第一個等待的位置,那么這個時候是可以去搶占鎖的。

  1. 如果已經搶占了鎖的線程就是當前線程。這種情況我們叫做重入。
    示例如下:
ReentrantLock lock = new ReentrantLock();
try {
    // 加鎖
    lock.lock();
    // 執行業務邏輯
    System.out.println("獲取的鎖");
    try {
        // 再次獲取鎖
        lock.lock();
        // 執行業務邏輯
        System.out.println("再次獲取的鎖");
    } finally {
        // 解鎖
        lock.unlock();
    }
} finally {
    // 解鎖
    lock.unlock();
}

小結一下:

  1. 如果當前鎖未被搶占,并且沒有其他線程等待,那么直接搶占鎖
  2. 如果當前鎖未被搶占,有其他線程等待,不可用搶占鎖
  3. 如果當前鎖被當前線程搶占了,那么直接重入即可
  4. 不符合上述情況,直接加鎖失敗。也就是鎖被其他線程搶占了,或者目前還有其他線程處于等待中,都會導致公平鎖加鎖失敗。
// 判斷等待隊列中是否有其他線程等待
public final boolean hasQueuedPredecessors() {
    Node h, s;
    // 如果等待隊列頭節點不為空,說明等待隊列已經創建出來了。否則直接返回false。
    if ((h = head) != null) {
        // 如果頭節點后面的節點為空,或者該節點的狀態是取消狀態
        if ((s = h.next) == null || s.waitStatus > 0) {
            s = null; // traverse in case of concurrent cancellation
            // 從后往前遍歷,直至最后一個狀態小于等于0的節點。只有小于等于0的節點才是正常的可以競爭鎖的節點。
            for (Node p = tail; p != h && p != null; p = p.prev) {
                // 發現小于等于0的節點,就賦值給s
                if (p.waitStatus <= 0)
                    s = p;
            }
        }
        // 如果最終得到的節點不為空。有可能當前沒有任何等待的節點,s=null。
        // 并且這個不為空的等待線程不是當前線程。其實就是說明前面還有其他線程排隊。
        if (s != null && s.thread != Thread.currentThread())
            // 返回true,說明有其他線程在排隊。
            return true;
    }
    // 1.如果等待隊列不存在,直接返回false
    // 2.如果當前等待隊列中,沒有任何其他節點的waitStatus<=0
    return false;
}

至此,線程競爭鎖的邏輯就完畢了。

  • 加入等待隊列
private Node addWaiter(Node mode) {
    // 創建一個節點,該節點默認
    // waitStatus=0, thread=currentThread
    Node node = new Node(mode);
    // 開啟自旋
    for (;;) {
        // 取出尾節點
        Node oldTail = tail;
        // 如果尾節點不為空
        if (oldTail != null) {
            // 設置node的前一個節點為尾節點
            node.setPrevRelaxed(oldTail);
            // CAS把尾節點設置為node
            if (compareAndSetTail(oldTail, node)){
                // 如果CAS設置成功,那么就把oldTail的next引用設置成node
                oldTail.next = node;
                // 返回node節點
                return node;
            }
        } else {
            // 如果尾節點為null,說明等待隊列還不存在,這個時候就要準備初始化等待隊列。
            // 初始化完畢后繼續自旋,最終把新創建的節點添加進等待隊列
            initializeSyncQueue();
        }
    }
}
// 初始化等待隊列。其實是一個雙向鏈表,所以只要初始化head、tail節點即可。
private final void initializeSyncQueue() {
    Node h;
    // CAS設置head節點。如果head節點為null,就設置為new Node()。該node節點waitStatus=0,thread=null。
    if (HEAD.compareAndSet(this, null, (h = new Node())))
        // 頭節點設置成功后,尾節點初始化為同一個節點。
        tail = h;
}
  1. 初始化等待隊列
// 初始化等待隊列。其實是一個雙向鏈表,所以只要初始化head、tail節點即可。
private final void initializeSyncQueue() {
    Node h;
    // CAS設置head節點。如果head節點為null,就設置為new Node()。該node節點waitStatus=0,thread=null。
    if (HEAD.compareAndSet(this, null, (h = new Node())))
        // 頭節點設置成功后,尾節點初始化為同一個節點。
        tail = h;
}
image.png
  1. 添加新的節點
// 創建新節點
Node node = new Node(mode);
// 取出尾節點
Node oldTail = tail;
image.png
// 設置node的前一個節點為尾節點
node.setPrevRelaxed(oldTail);
// CAS把尾節點設置為node
if (compareAndSetTail(oldTail, node)){
// 如果CAS設置成功,那么就把oldTail的next引用設置成node
oldTail.next = node;
image.png

經過上面幾步,新的節點就被添加到等待隊列中了。
有一個注意點需要提的是:

為什么判斷等待隊列是否存在,使用的是if(tail!=null),而不是if(head!=null)?

這個問題其實跟初始化等待隊列有關系,初始化的時候是使用CAS設置head節點,成功后再設置tail節點。也就是說,隊列初始化完畢的標識是tail!=null。
如果使用if(head!=null)來判斷隊列已經存在,那么有可能此時tail還沒有初始化完畢。就會導致使用tail節點的時候空指針異常。

  • 阻塞等待
final boolean acquireQueued(final Node node, int arg) {
    // 默認線程未被打斷
    boolean interrupted = false;
    try {
        // 開啟自旋
        for (;;) {
            // 獲取當前節點的前一個節點
            final Node p = node.predecessor();
            // 如果前一個節點是head節點,那么就嘗試競爭鎖
            if (p == head && tryAcquire(arg)) {
                // 競爭鎖成功,把當前節點設置為head節點
                setHead(node);
                // 把前一個節點和當前節點斷開
                // 因為當前節點已經設置為head節點了,之前的head就可以GC了
                p.next = null; // help GC
                // 返回是否當前線程被打斷。
                // 這個返回結果的作用會被用在lockInterruptibly()這個方法上。
                // lock()方法可忽略。
                return interrupted;
            }
            // 判斷當前節點是否應該阻塞。
            if (shouldParkAfterFailedAcquire(p, node))
                // 下面這個代碼可以翻譯成:
                // if(parkAndCheckInterrupt()){
                //     interrupted = true;
                // }
                interrupted |= parkAndCheckInterrupt();
        }
    } catch (Throwable t) {
        // 拋出任何異常,都直接取消當前節點正在競爭鎖的操作
        // 如果在等待隊列中,就從等待隊列中移除。
        // 如果當前線程已經搶占到鎖了,那么就解鎖。
        cancelAcquire(node);
        // 如果當前線程已經被中斷
        if (interrupted)
            // 重新設置中斷信號
            selfInterrupt();
        // 拋出當前異常
        throw t;
    }
}
  1. 獲取當前節點的上一個節點
// 獲取當前節點的前一個節點
final Node p = node.predecessor();

final Node predecessor() {
    // 上一個節點
    Node p = prev;
    // 如果為null,直接拋異常
    if (p == null)
        throw new NullPointerException();
    else
        // 返回上一個節點
        return p;
}
  1. 如果上一個節點為head節點
// 獲取當前節點的前一個節點
final Node p = node.predecessor();
// 如果前一個節點是head節點,那么就嘗試競爭鎖
if (p == head && tryAcquire(arg))
image.png
  1. 搶占成功鎖后
// 競爭鎖成功,把當前節點設置為head節點
setHead(node);
// 把前一個節點和當前節點斷開
p.next = null;
image.png
  1. 判斷當前節點的狀態
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
    // 獲取前一個節點的狀態
    int ws = pred.waitStatus;
    // 如果狀態等于-1。Node.SIGNAL的值就是-1
    if (ws == Node.SIGNAL)
        // 直接返回true,這個時候就要準備阻塞。
        return true;
    // 如果狀態值大于0,說明是要取消的節點。
    if (ws > 0) {
        // 跳過“取消”狀態節點
        do {
            node.prev = pred = pred.prev;
        } while (pred.waitStatus > 0);
        
        pred.next = node;
    } else {
        // ws小于等于0的話,直接把前一個節點的狀態置為-1
        // 因為新創建的節點初始化狀態是0,
        // 那么意味著執行到這里后,還要返回去重新自旋一次才能返回true。
        pred.compareAndSetWaitStatus(ws, Node.SIGNAL);
    }
    // 返回false
    return false;
}
  1. 當前線程阻塞
private final boolean parkAndCheckInterrupt() {
    // 阻塞當前線程。
    // 1. 調用LockSupport.unpark()才能重新喚醒被阻塞的線程。
    // 2.調用thread.interrupt()也可以喚醒阻塞線程。
    LockSupport.park(this);
    // 判斷當前線程是否被打斷。
    // 如果當前線程是被打斷的,那么返回true,否則返回false。
    return Thread.interrupted();
}

小結一下:

  1. 先獲取當前節點的前一個節點,如果是head節點,那么嘗試競爭鎖
    1. 競爭鎖成功后,重置head節點,返回false(代表沒有被打斷)。
  2. 如果前一個節點狀態小于等于0,那么置為-1。
    1. 重新自旋一次,從第一步開始
    2. 如果前一個節點狀態等于-1,返回true,準備阻塞。
  3. 調用LockSupport.park()阻塞當前線程,直至unpark()或者interrupt()喚醒當前線程。
    1. 通過unpark()喚醒,沒有被打斷,返回false
    2. 通過interrupt()喚醒,被打斷,返回true。
  4. 被喚醒的線程又開始自旋,直至獲取到鎖后返回是否被打斷的結果。
    1. 如果是被打斷后獲取鎖返回,那么返回true。
    2. 否則返回false。
public final void acquire(int arg) {
    // 嘗試獲取鎖
    if (!tryAcquire(arg) &&
    // addWaiter(Node.EXCLUSIVE):競爭鎖失敗后,添加到等待隊列
    // acquireQueued(node, arg):阻塞等待,自旋獲取鎖后,返回判斷是否被打斷
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        // 如果被打斷,需要恢復中斷信號
        selfInterrupt();
}

// 其實就是重新中斷一次。
// 因為執行過Thread.interrupted()方法后,會讓中斷信號重置為false。
static void selfInterrupt() {
    Thread.currentThread().interrupt();
}

以上就是我對于公平鎖-加鎖實現的淺析。

?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,488評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,034評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,327評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,554評論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,337評論 6 404
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,883評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 42,975評論 3 439
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,114評論 0 286
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,625評論 1 332
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,555評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,737評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,244評論 5 355
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 43,973評論 3 345
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,362評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,615評論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,343評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,699評論 2 370

推薦閱讀更多精彩內容