深刻理解ReentrantLock的實現原理

image

ReentrantLock簡介

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

AQS回顧

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

ReentrantLock原理

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

ReentrantLock結構

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

ReentrantLock有個重要的成員變量:bash

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爲參數的構造方法建立的是公平鎖ide

非公平鎖的實現原理

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

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而後線程會進入到同步隊列中

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,不喚醒同步隊列。

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實現了鎖獲取的公平性,即鎖的獲取按照先來先得的順序,後來的不能搶先獲取鎖,非公平鎖和公平鎖也正是經過這個區別來實現了鎖的公平性。學習

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鎖的釋放

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

lockInterruptibly可中斷方式獲取鎖

ReentrantLock相對於Synchronized擁有一些更方便的特性,好比能夠中斷的方式去獲取鎖。this

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,可是不會拋出異常

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從同步隊列轉移到條件隊列中。

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

由於只有在同步隊列中的線程才能去獲取鎖,因此經過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/…

相關文章
相關標籤/搜索