深入理解ReentrantLock的實現原理

薛8發表於2019-03-24
image

ReentrantLock簡介

ReentrantLockJavaJDK1.5引入的顯式鎖,在實現原理和功能上都和內建鎖(synchronized)上都有區別,在文章最後我們再比較這兩個鎖。
首先我們要知道ReentrantLock是基於AQS實現的,所以我們得對AQS有所瞭解才能更好的去學習掌握ReentrantLock,關於AQS的介紹可以參考我之前寫的一篇文章《一文帶你快速掌握AQS》,這裡簡單回顧下AQS

AQS回顧

AQSAbstractQueuedSynchronizer的縮寫,這個是個內部實現了兩個佇列的抽象類,分別是同步佇列條件佇列。其中同步佇列是一個雙向連結串列,裡面儲存的是處於等待狀態的執行緒,正在排隊等待喚醒去獲取鎖,而條件佇列是一個單向連結串列,裡面儲存的也是處於等待狀態的執行緒,只不過這些執行緒喚醒的結果是加入到了同步佇列的隊尾,AQS所做的就是管理這兩個佇列裡面執行緒之間的等待狀態-喚醒的工作。
在同步佇列中,還存在2中模式,分別是獨佔模式共享模式,這兩種模式的區別就在於AQS在喚醒執行緒節點的時候是不是傳遞喚醒,這兩種模式分別對應獨佔鎖共享鎖
AQS是一個抽象類,所以不能直接例項化,當我們需要實現一個自定義鎖的時候可以去繼承AQS然後重寫獲取鎖的方式釋放鎖的方式還有管理state,而ReentrantLock就是通過重寫了AQStryAcquiretryRelease方法實現的lockunlock

深入理解ReentrantLock的實現原理
深入理解ReentrantLock的實現原理

ReentrantLock原理

通過前面的回顧,是不是對ReentrantLock有了一定的瞭解了,ReentrantLock通過重寫鎖獲取方式鎖釋放方式這兩個方法實現了公平鎖非公平鎖,那麼ReentrantLock是怎麼重寫的呢,這也就是本節需要探討的問題。

ReentrantLock結構

深入理解ReentrantLock的實現原理
首先ReentrantLock繼承自父類Lock,然後有3個內部類,其中Sync內部類繼承自AQS,另外的兩個內部類繼承自Sync,這兩個類分別是用來公平鎖和非公平鎖的。
通過Sync重寫的方法tryAcquiretryRelease可以知道,ReentrantLock實現的是AQS的獨佔模式,也就是獨佔鎖,這個鎖是悲觀鎖

ReentrantLock有個重要的成員變數:

private final Sync sync;
複製程式碼

這個變數是用來指向Sync的子類的,也就是FairSync或者NonfairSync,這個也就是多型的父類引用指向子類,具體Sycn指向哪個子類,看構造方法:

public ReentrantLock() {
    sync = new NonfairSync();
}

public ReentrantLock(boolean fair) {
    sync = fair ? new FairSync() : new NonfairSync();
}
複製程式碼

ReentrantLock有兩個構造方法,無參構造方法預設是建立非公平鎖,而傳入true為引數的構造方法建立的是公平鎖

非公平鎖的實現原理

當我們使用無參構造方法構造的時候即ReentrantLock lock = new ReentrantLock(),建立的就是非公平鎖。

public ReentrantLock() {
    sync = new NonfairSync();
}

//或者傳入false引數 建立的也是非公平鎖
public ReentrantLock(boolean fair) {
    sync = fair ? new FairSync() : new NonfairSync();
}
複製程式碼

lock方法獲取鎖

  1. lock方法呼叫CAS方法設定state的值,如果state等於期望值0(代表鎖沒有被佔用),那麼就將state更新為1(代表該執行緒獲取鎖成功),然後執行setExclusiveOwnerThread方法直接將該執行緒設定成鎖的所有者。如果CAS設定state的值失敗,即state不等於0,代表鎖正在被佔領著,則執行acquire(1),即下面的步驟。
  2. nonfairTryAcquire方法首先呼叫getState方法獲取state的值,如果state的值為0(之前佔領鎖的執行緒剛好釋放了鎖),那麼用CAS這是state的值,設定成功則將該執行緒設定成鎖的所有者,並且返回true。如果state的值不為0,那就呼叫getExclusiveOwnerThread方法檢視佔用鎖的執行緒是不是自己,如果是的話那就直接將state + 1,然後返回true。如果state不為0且鎖的所有者又不是自己,那就返回false然後執行緒會進入到同步佇列中

深入理解ReentrantLock的實現原理

final void lock() {
    //CAS操作設定state的值
    if (compareAndSetState(0, 1))
        //設定成功 直接將鎖的所有者設定為當前執行緒 流程結束
        setExclusiveOwnerThread(Thread.currentThread());
    else
        //設定失敗 則進行後續的加入同步佇列準備
        acquire(1);
}

public final void acquire(int arg) {
    //呼叫子類重寫的tryAcquire方法 如果tryAcquire方法返回false 那麼執行緒就會進入同步佇列
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}

//子類重寫的tryAcquire方法
protected final boolean tryAcquire(int acquires) {
    //呼叫nonfairTryAcquire方法
    return nonfairTryAcquire(acquires);
}

final boolean nonfairTryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();
    //如果狀態state=0,即在這段時間內 鎖的所有者把鎖釋放了 那麼這裡state就為0
    if (c == 0) {
        //使用CAS操作設定state的值
        if (compareAndSetState(0, acquires)) {
            //操作成功 則將鎖的所有者設定成當前執行緒 且返回true,也就是當前執行緒不會進入同步
            //佇列。
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    //如果狀態state不等於0,也就是有執行緒正在佔用鎖,那麼先檢查一下這個執行緒是不是自己
    else if (current == getExclusiveOwnerThread()) {
        //如果執行緒就是自己了,那麼直接將state+1,返回true,不需要再獲取鎖 因為鎖就在自己
        //身上了。
        int nextc = c + acquires;
        if (nextc < 0) // overflow
            throw new Error("Maximum lock count exceeded");
        setState(nextc);
        return true;
    }
    //如果state不等於0,且鎖的所有者又不是自己,那麼執行緒就會進入到同步佇列。
    return false;
}
複製程式碼

tryRelease鎖的釋放

  1. 判斷當前執行緒是不是鎖的所有者,如果是則進行步驟2,如果不是則丟擲異常。
  2. 判斷此次釋放鎖後state的值是否為0,如果是則代表鎖有沒有重入,然後將鎖的所有者設定成null且返回true,然後執行步驟3,如果不是則代表鎖發生了重入執行步驟4
  3. 現在鎖已經釋放完,即state=0,喚醒同步佇列中的後繼節點進行鎖的獲取。
  4. 鎖還沒有釋放完,即state!=0,不喚醒同步佇列。

深入理解ReentrantLock的實現原理

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

public final boolean release(int arg) {
    //子類重寫的tryRelease方法,需要等鎖的state=0,即tryRelease返回true的時候,才會去喚醒其
    //它執行緒進行嘗試獲取鎖。
    if (tryRelease(arg)) {
        Node h = head;
        if (h != null && h.waitStatus != 0)
            unparkSuccessor(h);
        return true;
    }
    return false;
}
    
protected final boolean tryRelease(int releases) {
    //狀態的state減去releases
    int c = getState() - releases;
    //判斷鎖的所有者是不是該執行緒
    if (Thread.currentThread() != getExclusiveOwnerThread())
        //如果所的所有者不是該執行緒 則丟擲異常 也就是鎖釋放的前提是執行緒擁有這個鎖,
        throw new IllegalMonitorStateException();
    boolean free = false;
    //如果該執行緒釋放鎖之後 狀態state=0,即鎖沒有重入,那麼直接將將鎖的所有者設定成null
    //並且返回true,即代表可以喚醒其他執行緒去獲取鎖了。如果該執行緒釋放鎖之後state不等於0,
    //那麼代表鎖重入了,返回false,代表鎖還未正在釋放,不用去喚醒其他執行緒。
    if (c == 0) {
        free = true;
        setExclusiveOwnerThread(null);
    }
    setState(c);
    return free;
}
複製程式碼

公平鎖的實現原理

lock方法獲取鎖

  1. 獲取狀態的state的值,如果state=0即代表鎖沒有被其它執行緒佔用(但是並不代表同步佇列沒有執行緒在等待),執行步驟2。如果state!=0則代表鎖正在被其它執行緒佔用,執行步驟3
  2. 判斷同步佇列是否存線上程(節點),如果不存在則直接將鎖的所有者設定成當前執行緒,且更新狀態state,然後返回true。
  3. 判斷鎖的所有者是不是當前執行緒,如果是則更新狀態state的值,然後返回true,如果不是,那麼返回false,即執行緒會被加入到同步佇列中

通過步驟2實現了鎖獲取的公平性,即鎖的獲取按照先來先得的順序,後來的不能搶先獲取鎖,非公平鎖和公平鎖也正是通過這個區別來實現了鎖的公平性。

深入理解ReentrantLock的實現原理

final void lock() {
    acquire(1);
}

public final void acquire(int arg) {
    //同步佇列中有執行緒 且 鎖的所有者不是當前執行緒那麼將執行緒加入到同步佇列的尾部,
    //保證了公平性,也就是先來的執行緒先獲得鎖,後來的不能搶先獲取。
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}

protected final boolean tryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();
    //判斷狀態state是否等於0,等於0代表鎖沒有被佔用,不等於0則代表鎖被佔用著。
    if (c == 0) {
        //呼叫hasQueuedPredecessors方法判斷同步佇列中是否有執行緒在等待,如果同步佇列中沒有
        //執行緒在等待 則當前執行緒成為鎖的所有者,如果同步佇列中有執行緒在等待,則繼續往下執行
        //這個機制就是公平鎖的機制,也就是先讓先來的執行緒獲取鎖,後來的不能搶先獲取。
        if (!hasQueuedPredecessors() &&
            compareAndSetState(0, acquires)) {
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    //判斷當前執行緒是否為鎖的所有者,如果是,那麼直接更新狀態state,然後返回trueelse if (current == getExclusiveOwnerThread()) {
        int nextc = c + acquires;
        if (nextc < 0)
            throw new Error("Maximum lock count exceeded");
        setState(nextc);
        return true;
    }
    //如果同步佇列中有執行緒存在 且 鎖的所有者不是當前執行緒,則返回falsereturn false;
}
複製程式碼

tryRelease鎖的釋放

公平鎖的釋放和非公平鎖的釋放一樣,這裡就不重複。
公平鎖和非公平鎖的公平性是在獲取鎖的時候體現出來的,釋放的時候都是一樣釋放的。

lockInterruptibly可中斷方式獲取鎖

ReentrantLock相對於Synchronized擁有一些更方便的特性,比如可以中斷的方式去獲取鎖。

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

public final void acquireInterruptibly(int arg)
        throws InterruptedException {
    //如果當前執行緒已經中斷了,那麼丟擲異常
    if (Thread.interrupted())
        throw new InterruptedException();
    //如果當前執行緒仍然未成功獲取鎖,則呼叫doAcquireInterruptibly方法,這個方法和
    //acquireQueued方法沒什麼區別,就是執行緒在等待狀態的過程中,如果執行緒被中斷,執行緒會
    //丟擲異常。
    if (!tryAcquire(arg))
        doAcquireInterruptibly(arg);
}
複製程式碼

tryLock超時等待方式獲取鎖

ReentrantLock除了能以能中斷的方式去獲取鎖,還可以以超時等待的方式去獲取鎖,所謂超時等待就是執行緒如果在超時時間內沒有獲取到鎖,那麼就會返回false,而不是一直"死迴圈"獲取。

  1. 判斷當前節點是否已經中斷,已經被中斷過則丟擲異常,如果沒有被中斷過則嘗試獲取鎖,獲取失敗則呼叫doAcquireNanos方法使用超時等待的方式獲取鎖。
  2. 將當前節點封裝成獨佔模式的節點加入到同步佇列的隊尾中。
  3. 進入到"死迴圈"中,但是這個死迴圈是有個限制的,也就是當執行緒達到超時時間了仍未獲得鎖,那麼就會返回false,結束迴圈。這裡呼叫的是LockSupport.parkNanos方法,在超時時間內沒有被中斷,那麼執行緒會從超時等待狀態轉成了就緒狀態,然後被CPU排程繼續執行迴圈,而這時候執行緒已經達到超時等到的時間,返回false

LockSuport的方法能響應Thread.interrupt,但是不會丟擲異常

深入理解ReentrantLock的實現原理

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();
    //再嘗試獲取一次 如果不成功則呼叫doAcquireNanos方法進行超時等待獲取鎖
    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;
    //呼叫addWaiter將當前執行緒封裝成獨佔模式的節點 並且加入到同步佇列尾部
    final Node node = addWaiter(Node.EXCLUSIVE);
    boolean failed = true;
    try {
        for (;;) {
            final Node p = node.predecessor();
            //如果當前節點的前驅節點為頭結點 則讓當前節點去嘗試獲取鎖。
            if (p == head && tryAcquire(arg)) {
                //當前節點獲取鎖成功 則將當前節點設定為頭結點,然後返回truesetHead(node);
                p.next = null; // help GC
                failed = false;
                return true;
            }
            //如果當前節點的前驅節點不是頭結點 或者 當前節點獲取鎖失敗,
            //則再次判斷當前執行緒是否已經超時。
            nanosTimeout = deadline - System.nanoTime();
            if (nanosTimeout <= 0L)
                return false;
            //呼叫shouldParkAfterFailedAcquire方法,告訴當前節點的前驅節點 我要進入
            //等待狀態了,到我了記得喊我,即做好進入等待狀態前的準備。
            if (shouldParkAfterFailedAcquire(p, node) &&
                nanosTimeout > spinForTimeoutThreshold)
                //呼叫LockSupport.parkNanos方法,將當前執行緒設定成超時等待的狀態。
                LockSupport.parkNanos(this, nanosTimeout);
            if (Thread.interrupted())
                throw new InterruptedException();
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}
複製程式碼

ReentrantLock的等待/通知機制

我們知道關鍵字Synchronized + ObjectwaitnotifynotifyAll方法能實現等待/通知機制,那麼ReentrantLock是否也能實現這樣的等待/通知機制,答案是:可以。
ReentrantLock通過Condition物件,也就是條件佇列實現了和waitnotifynotifyAll相同的語義。 執行緒執行condition.await()方法,將節點1從同步佇列轉移到條件佇列中。

深入理解ReentrantLock的實現原理

執行緒執行condition.signal()方法,將節點1從條件佇列中轉移到同步佇列。

深入理解ReentrantLock的實現原理

因為只有在同步佇列中的執行緒才能去獲取鎖,所以通過Condition物件的waitsignal方法能實現等待/通知機制。
程式碼示例:

ReentrantLock lock = new ReentrantLock();
Condition condition = lock.newCondition();
public void await() {
    lock.lock();
    try {
        System.out.println("執行緒獲取鎖----" + Thread.currentThread().getName());
        condition.await(); //呼叫await()方法 會釋放鎖,和Object.wait()效果一樣。
        System.out.println("執行緒被喚醒----" + Thread.currentThread().getName());
    } catch (InterruptedException e) {
        e.printStackTrace();
    } finally {
        lock.unlock();
        System.out.println("執行緒釋放鎖----" + Thread.currentThread().getName());
    }
}

public void signal() {
    try {
        Thread.sleep(1000);  //休眠1秒鐘 等等一個執行緒先執行
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    lock.lock();
    try {
        System.out.println("另外一個執行緒獲取到鎖----" + Thread.currentThread().getName());
        condition.signal();
        System.out.println("喚醒執行緒----" + Thread.currentThread().getName());
    } finally {
        lock.unlock();
        System.out.println("另外一個執行緒釋放鎖----" + Thread.currentThread().getName());
    }
}

public static void main(String[] args) {
    Test t = new Test();
    Thread t1 = new Thread(new Runnable() {
        @Override
        public void run() {
            t.await();
        }
    });

    Thread t2 = new Thread(new Runnable() {
        @Override
        public void run() {
            t.signal();
        }
    });

    t1.start();
    t2.start();
}
複製程式碼

執行輸出:

執行緒獲取鎖----Thread-0
另外一個執行緒獲取到鎖----Thread-1
喚醒執行緒----Thread-1
另外一個執行緒釋放鎖----Thread-1
執行緒被喚醒----Thread-0
執行緒釋放鎖----Thread-0
複製程式碼

執行的流程大概是這樣,執行緒t1先獲取到鎖,輸出了"執行緒獲取鎖----Thread-0",然後執行緒t1呼叫await方法,呼叫這個方法的結果就是執行緒t1釋放了鎖進入等待狀態,等待喚醒,接下來執行緒t2獲取到鎖,然輸出了"另外一個執行緒獲取到鎖----Thread-1",同時執行緒t2呼叫signal方法,呼叫這個方法的結果就是喚醒一個在條件佇列(Condition)的執行緒,然後執行緒t1被喚醒,而這個時候執行緒t2並沒有釋放鎖,執行緒t1也就沒法獲得鎖,等執行緒t2繼續執行輸出"喚醒執行緒----Thread-1"之後執行緒t2釋放鎖且輸出"另外一個執行緒釋放鎖----Thread-1",這時候執行緒t1獲得鎖,繼續往下執行輸出了執行緒被喚醒----Thread-0,然後釋放鎖輸出"執行緒釋放鎖----Thread-0"

如果想單獨喚醒部分執行緒應該怎麼做呢?這時就有必要使用多個Condition物件了,因為ReentrantLock支援建立多個Condition物件,例如:

//為了減少篇幅 僅給出虛擬碼
ReentrantLock lock = new ReentrantLock();
Condition condition = lock.newCondition();
Condition condition1 = lock.newCondition();

//執行緒1 呼叫condition.await() 執行緒進入到條件佇列
condition.await();

//執行緒2 呼叫condition1.await() 執行緒進入到條件佇列
condition1.await();

//執行緒32 呼叫condition.signal() 僅喚醒呼叫condition中的執行緒,不會影響到呼叫condition1。
condition1.await();
複製程式碼

這樣就實現了部分喚醒的功能。

ReentrantLock和Synchronized對比

關於Synchronized的介紹可以看《synchronized的使用(一)》《深入分析synchronized原理和鎖膨脹過程(二)》

ReentrantLock Synchronized
底層實現 通過AQS實現 通過JVM實現,其中synchronized又有多個型別的鎖,除了重量級鎖是通過monitor物件(作業系統mutex互斥原語)實現外,其它型別的通過物件頭實現。
是否可重入
公平鎖
非公平鎖
鎖的型別 悲觀鎖、顯式鎖 悲觀鎖、隱式鎖(內建鎖)
是否支援中斷
是否支援超時等待
是否自動獲取/釋放鎖

參考

《Java併發程式設計的藝術》
深入理解AbstractQueuedSynchronizer(AQS)
Java 重入鎖 ReentrantLock 原理分析)

原文地址:ddnd.cn/2019/03/24/…

相關文章