Java鎖Lock源碼分析(一)

Java中的鎖Lock源碼分析(一)

Java中的鎖有很多,同時也是了整個併發包的基礎,可以說明白了鎖整個併發包你也就能明白一半了,如果之前你有所瞭解的話java中的鎖你或許對這些名詞有些概念:

  • 獨佔鎖、共享鎖
  • 公平鎖、非公平鎖、重入鎖
  • 條件鎖
  • 讀寫鎖

本節要點:

0)鎖是如何表示的(怎麼樣就代表獲取到了鎖)
1)volatile在作用
2)lock的源碼分析
3)重入鎖是如何做到的
4)公平鎖與非公平鎖的區別

我們使用ReentrantLock的方式很簡單在方法體內lock.lock();finally中執行lock.unlock()方法,非常簡單。
進入源碼可以看到我們常說的AQS(AbstractQueuedSynchronizer)

lock方法:

    public void lock() {
        sync.lock();
    }

這裏寫圖片描述

ASQ的成員 state int類型 volatile修飾

    private volatile int state;

這裏寫圖片描述

CAS(就是在操作AQS的state字段)最終也是調用sun.misc.Unsafe相關的方法,這個方法的四個參數第一個表示操作的是那個對象,第二個表示操作對象字段的偏移量,第三個是期望值,第四個是更新值
這裏寫圖片描述
lock方法返回就是獲取了鎖,即上圖CAS設置整個state+1返回true,就說明獲取到了鎖。

要點0的答案:AQS的volatile int state +1表示獲取到了鎖。

通過cas設置AQS的成員status,大家注意到status是用volatile來修飾的,它在此處表示讓所線程能夠獲取到最新更改的值。

要點1的答案

a: 保證變量在線程之間的可見性 就是上面說的
b:禁止指令重排序 在編譯階段插入內存屏障,來特定禁止指令重排序

我來先畫張圖表示下主存中變量和方法棧中的變量關係:
我們知道對象的成員是跟對象在堆(主存)中,方法運行時在棧中的
這裏寫圖片描述

線程在棧中運行方法修改變量的時候會從主存中拷貝一個副本到自己的棧中,當方法修改變量執行返回會把最新值寫會到主存。

1)沒有使用volatile修飾時,thread1和thread2同時執行同一個方法來修改state爲1(cas的第三個值expcet=0,update=1),那麼當thread1返回之後寫會到主存,thread2沒有感知到還認爲是expect=0,update=1 ,thread2中就是老的數據此次cas應該是也會成功,修改了相當於是成功獲取到了鎖 ,對於獨佔鎖來說肯定是不對的。

2)使用volatile修飾時,當thread1寫會成功之後會讓其它線程中該變量的副本失效,並重新從主存load,這樣一來thread2 expect=0,update=1就會失敗,因爲此時的expect=0是不成立的,此時的state已經是1瞭如下圖。
這裏寫圖片描述

volatile在此處的就是保證變量被修改的最新值,能夠被其它線程感知。

要點2源碼分析

ReentrantLock改造方法有個參數能決定是否是公平所

 public ReentrantLock(boolean fair) {
     sync = fair ? new FairSync() : new NonfairSync();
 }

公平鎖lock方法如下:
這裏寫圖片描述

非公平鎖lock方法:
這裏寫圖片描述

剛開始看的時候我一直感覺不到公平鎖和非公平鎖到底區別在哪裏???

要點4的答案的第一部分

公平鎖與非公平鎖基本一樣其實在方法的最外層就可以看到:
非公平鎖先進性一次CAS搶佔

要點4的答案的第二部分
公平鎖先判斷隊列(雙向鏈表)爲空(head==tail)在進行cas搶佔
最終兩者爲獲取所的線程都會進入到隊列中,稍後你會看到。

AQS的模板方法acquire(args)
它是ReentrantLock成員Sync的整個鎖的邏輯,所有的類型的都是基於這個模板方法實現的:
AQS模板方法acquire

我們以三個線程thread1,thread2,thread3同時執行lock.lock()方法展開源碼的分析,以爲例非公平鎖:

lock方法如下:
這裏寫圖片描述

thread1,thread2,thread3三個線程同時進入方法,都先進行一次CAS獲取鎖一下,設置state,expect=0,update=1,
假設thread1設置成功,那麼獨佔線程設置爲當前thread1,獲取到了鎖lock方法退出,thread1就可以執行業務邏輯了。

thread2和thread3都會進入else即acquire(1)方法,
成員Sync提供了整個的鎖的邏輯,acquire() 爲AQS實現鎖邏輯的模板方法
這裏寫圖片描述

tryAcquire方法在非公平鎖實現中調用了nonfairTryAcquire
這裏寫圖片描述

三個線程我們還是一個一個來分析:
thread1進入acquire(1):
如果再次調用了lock.lock()那麼還是同樣會進入到acquire進入到nonfairAcquire(1) 此時getState() 1
當前線程
獨佔線程 再次將state+1了,此時state thread1此時就表明了重入了,nonfairTryAcquire返回true,tryAcquire返回true,acquire方法退出,lock方法退出,thread1成功的再次獲取到鎖,state=2了。

要點3的答案:獲取鎖的線程,還能在獲取鎖就表示重入

thread2進入acquire(1):
我們假設thread1是持有鎖的線程,那麼本次的tryAcqurie返回了false。進入到addWaiter(Node.EXCLUSIVE)方法。
我們先看下Node的結構:

static final class Node {
        static final Node SHARED = new Node();
        static final Node EXCLUSIVE = null;
        static final int CANCELLED =  1;
        static final int SIGNAL    = -1;
        static final int CONDITION = -2;
        static final int PROPAGATE = -3;
        volatile int waitStatus;
        volatile Node prev;
        volatile Node next;
        volatile Thread thread;
        Node nextWaiter;
        Node() {    // Used to establish initial head or SHARED marker
        }
        Node(Thread thread, Node mode) {     // Used by addWaiter
            this.nextWaiter = mode;
            this.thread = thread;
        }
        Node(Thread thread, int waitStatus) { // Used by Condition
            this.waitStatus = waitStatus;
            this.thread = thread;
        }
    }

預先透漏下這就是一個FIFO的雙向鏈表,這裏waiterStatus是進入到鏈表之後決定是否可以嘗試獲取鎖,每個節點的等待狀態的。
前面的state是嘗試獲取鎖的
這裏的waitStatus是等待鏈表(隊列)中的狀態,不要記混了。
waitStatus有五個值:1表示鏈表中節點取消,0表示鏈表中節點初始狀態 ,-1表示鏈表中節點等待喚醒狀態,-2表示鏈表中節點是條件鎖(下一篇博文會說),-3表示傳播

addWaiter源碼

這裏寫圖片描述

這裏寫圖片描述

構造獨佔節點進入enq方法,進入死循環中,我們來看下每循環一次都有那些變化。

下圖爲addWaiter的一種場景(並不是一定會這樣,主要看線程執行到那個方法什麼時候入隊)就是一個簡單的入隊操作,入隊成功之後就自動返回了當前Node。

這裏寫圖片描述

這裏畫圖主要是看waitStatus跟下面的部分結合。

入隊之後然後就是自旋獲取鎖的部分,代碼:
所謂自旋

不是一直在執行這段邏輯 head.next 有機會tryAcquire一次,

成功則獲取鎖就結束
失敗則跟其它線程一樣 修改prev.waitStatus=-1,在tryAcquire一次,如果在失敗就自己park住

這裏寫圖片描述

哎,又是一個死循環,來看看邏輯是如何的。
上圖的節點入隊是跟這裏交叉的,也就是說thread2入隊的時候,thread3也剛入隊,兩個線程也可能是同時同時進入acquireQueued的也有可能是thread2進入完了acquireQueued,thread3纔剛執行addWaiter,是不確定的,但不論如何最終執行邏輯都是一樣的(不管是並行的還是先後的)。

假設這裏是thread2執行,獲取node到prev如果爲head則再次執行tryAcquire(1)如果獲取到的話,thread2的node設置爲隊列的頭結點,thread2獲取返回,acquireQueued返回,acquire返回,lock返回,thread2執行業務邏輯。
如果tryAcquire(1)失敗或者不是node.prev!=head(比如thread3的node),進入方法shouldParkAfterFailedAcquire(prevNode,node)
這裏寫圖片描述
ws>0的情況其實是取消了節點,圖中ws的循環時將取消的節點移除掉。

這裏我們以thread3執行爲例(那個都一樣),還記得Node.waitStatus的那5個值嗎?

waitStatus有五個值:
1表示鏈表中節點取消,
0表示鏈表中節點初始狀態 ,
-1表示鏈表中節點等待喚醒狀態,
-2表示鏈表中節點是條件鎖(下一篇博文會說),
-3表示傳播

shouldParkAfterFailAcquire是在外層的死循環中if語句中被多次調用的,還是我們在來看看那每輪的結果。

這裏寫圖片描述

此時thread2和thread3都LockSupport.park(this)掛起了,阻塞住了,等待thread1喚醒,看看thread1是如何喚醒掛起線程的
lock.unlock方法的執行邏輯
這裏寫圖片描述

就兩個部分tryRelease(1)和unparkSuccessor(head),也就是對應了兩個操作,釋放鎖,喚醒下一個等待節點

釋放鎖:
這裏寫圖片描述
加鎖的時候是對state進行加操作,release進行減操作。
重入幾次即state>0,就要釋放幾次,很簡單明瞭。

喚醒頭結點下一個未取消的節點 upparkSuccessor(head);
這裏寫圖片描述

將head.waitStatus設置爲0,找到第一個head.next.waitStatus<=0,將其喚醒。

喚醒了之後還需要從等待隊列tryAccquire來搶佔,再回到acquireQueued
這裏寫圖片描述

此時的等待節點的狀態如下:

這裏寫圖片描述
ps:head.waitStatus=0和-1都是可能的

0 t2正在tryAccquire,t1剛釋放,成功
-1 t2tryAcquire失敗,將prev.waitStatus=-1,再由t1釋放的時候,喚醒t2

此時thread2的node.prev==head && tryAcquire(1)便能夠成功,將thread2設置爲設置爲head從等待隊列中摘除掉。

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

thread2的acquireQueued方法返回,acquire方法返回,lock.lock()方法返回,thread2執行業務邏輯。
此時隊列等待狀態如下:
這裏寫圖片描述

整個流程的閉環就結束了。

補充:
上面講的lock()方法如果當前線程獲取不到鎖,那麼該線程會一直阻塞。實際情況下還有如下需求:

1)嘗試獲取鎖一次,失敗了返回失敗,成功就返回成功,不阻塞,只是試一下。
2)超時嘗試,如果嘗試獲取鎖失敗了,一直重試,或者等一會再嘗試獲取鎖。

其實分別對應兩個方法:
tryLock()
tryLock(timeout,unit)

tryLock()方法
嘗試一下獲取鎖

public boolean tryLock() {
    return sync.nonfairTryAcquire(1);
}
final boolean nonfairTryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();
    if (c == 0) {
        if (compareAndSetState(0, acquires)) {
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    else if (current == getExclusiveOwnerThread()) {
        int nextc = c + acquires;
        if (nextc < 0) // overflow
            throw new Error("Maximum lock count exceeded");
        setState(nextc);
        return true;
    }
    return false;
}

太簡單了(非公平方式)
state=0(沒有線程獲取鎖)的時候cas一下
state>0(有線程獲取鎖)的時候判斷是不是自己獲取到的鎖,是就重入state++

tryLock(timeout,unit)方法

public boolean tryLock(long timeout, TimeUnit unit)
        throws InterruptedException {
    return sync.tryAcquireNanos(1, unit.toNanos(timeout));
}
public final boolean tryAcquireNanos(int arg, long nanosTimeout)
        throws InterruptedException {
    if (Thread.interrupted())
        throw new InterruptedException();
    return tryAcquire(arg) ||
        doAcquireNanos(arg, nanosTimeout);
}
private boolean doAcquireNanos(int arg, long nanosTimeout)
        throws InterruptedException {
    if (nanosTimeout <= 0L)
        return false;
    final long deadline = System.nanoTime() + nanosTimeout;//①
    final Node node = addWaiter(Node.EXCLUSIVE);
    boolean failed = true;
    try {
        for (;;) {
            final Node p = node.predecessor();
            if (p == head && tryAcquire(arg)) {
                setHead(node);
                p.next = null; // help GC
                failed = false;
                return true;
            }
            nanosTimeout = deadline - System.nanoTime();
            if (nanosTimeout <= 0L)
                return false;
            if (shouldParkAfterFailedAcquire(p, node) &&
                nanosTimeout > spinForTimeoutThreshold)
                LockSupport.parkNanos(this, nanosTimeout);//②
            if (Thread.interrupted())
                throw new InterruptedException();
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

邏輯也是很簡單:先放入阻塞隊列(FIFO雙向鏈表)
怎麼樣跟你像的不一樣吧不是一直在嘗試獲取鎖而是根據timeout時長來判定
1)等待時間小於1ms=1000納秒=spinForTimeoutThreshold=1000L,就無限嘗試直至超時
2)等待時間>1ms 就調用LockSupport.park(timeout),超時後,再次邏輯小於0返回fase,獲取鎖失敗
最長失敗的情況下都會講改Node從阻塞隊列(FIFO雙向鏈表)中移除掉。

嗯,看來jdk對某個線程阻塞的時間主要靠的LockSupport來支持,最終調用unsafe來調用操作系統的阻塞時間。

總結:

ReentrantLock的底層是AQS,通過控制state完成一些鎖特有的特性:重入、公平與非公平、讀寫鎖(後面的文章會說明)
獲取鎖就是當前線程成功修改了AQS的volatile成員state
獲取鎖失敗就進入到了AQS的等待隊列(FIFO的雙向無環鏈表),進入到等待隊列之後開始自旋,當前節點的waitStatus=-1之後lockSupport.park()掛起自己,等待喚醒
獲取鎖的線程釋放鎖(state執行減操作),喚醒head節點之後第一個未取消的等待節點
head節點之後第一個未取消的等待節點被喚醒,判斷prev是否爲head 是head則嘗試獲取所,將自己設置爲head節點,將原先老的head移除等待隊列。

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