溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務(wù)條款》

怎么深入理解ReentrantLock原理

發(fā)布時間:2021-12-03 16:05:02 來源:億速云 閱讀:164 作者:柒染 欄目:大數(shù)據(jù)

這期內(nèi)容當(dāng)中小編將會給大家?guī)碛嘘P(guān)怎么深入理解ReentrantLock原理,文章內(nèi)容豐富且以專業(yè)的角度為大家分析和敘述,閱讀完這篇文章希望大家可以有所收獲。

ReentrantLock是什么?

ReentrantLock是個典型的獨占模式AQS,同步狀態(tài)為0時表示空閑。當(dāng)有線程獲取到空閑的同步狀態(tài)時,它會將同步狀態(tài)加1,將同步狀態(tài)改為非空閑,于是其他線程掛起等待。在修改同步狀態(tài)的同時,并記錄下自己的線程,作為后續(xù)重入的依據(jù),即一個線程持有某個對象的鎖時,再次去獲取這個對象的鎖是可以成功的。如果是不可重入的鎖的話,就會造成死鎖。

ReentrantLock會涉及到公平鎖和非公平鎖,實現(xiàn)關(guān)鍵在于成員變量sync的實現(xiàn)不同,這是鎖實現(xiàn)互斥同步的核心。

 //公平鎖和非公平鎖的變量
 private final Sync sync;
 //父類
 abstract static class Sync extends AbstractQueuedSynchronizer {}
 //公平鎖子類
 static final class FairSync extends Sync {}
 //非公平鎖子類
 static final class NonfairSync extends Sync {}

那公平鎖和非公平鎖是什么?有什么區(qū)別?

那公平鎖和非公平鎖是什么?有什么區(qū)別?

公平鎖是指當(dāng)鎖可用時,在鎖上等待時間最長的線程將獲得鎖的使用權(quán),即先進(jìn)先出。而非公平鎖則隨機(jī)分配這種使用權(quán),是一種搶占機(jī)制,是隨機(jī)獲得鎖,并不是先來的一定能先得到鎖。

ReentrantLock提供了一個構(gòu)造方法,可以實現(xiàn)公平鎖或非公平鎖:

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

雖然公平鎖在公平性得以保障,但因為公平的獲取鎖沒有考慮到操作系統(tǒng)對線程的調(diào)度因素以及其他因素,會影響性能。

雖然非公平模式效率比較高,但是非公平模式在申請獲取鎖的線程足夠多,那么可能會造成某些線程長時間得不到鎖,這就是非公平鎖的“饑餓”問題。

但大部分情況下我們使用非公平鎖,因為其性能比公平鎖好很多。但是公平鎖能夠避免線程饑餓,某些情況下也很有用。

接下來看看ReentrantLock公平鎖的實現(xiàn):

ReentrantLock::lock公平鎖模式實現(xiàn)

怎么深入理解ReentrantLock原理

首先需要在構(gòu)建函數(shù)中傳入true創(chuàng)建好公平鎖

ReentrantLock reentrantLock = new ReentrantLock(true);

調(diào)用lock()進(jìn)行上鎖,直接acquire(1)上鎖

public void lock() {
    // 調(diào)用的sync的子類FairSync的lock()方法:ReentrantLock.FairSync.lock()
    sync.lock();
}
final void lock() {
    // 調(diào)用AQS的acquire()方法獲取鎖,傳的值為1
    acquire(1);
}

直接嘗試獲取鎖,

// AbstractQueuedSynchronizer.acquire()
public final void acquire(int arg) {
    // 嘗試獲取鎖
    // 如果失敗了,就排隊
    if (!tryAcquire(arg) &&
        // 注意addWaiter()這里傳入的節(jié)點模式為獨占模式
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}

具體獲取鎖流程

  • getState()獲取同步狀態(tài)state值,進(jìn)行判斷是否為0

    怎么深入理解ReentrantLock原理

    • 如果狀態(tài)變量的值為0,說明暫時還沒有人占有鎖, 使用hasQueuedPredecessors()保證了不論是新的線程還是已經(jīng)排隊的線程都順序使用鎖,如果沒有其它線程在排隊,那么當(dāng)前線程嘗試更新state的值為1,并自己設(shè)置到exclusiveOwnerThread變量中,供后續(xù)自己可重入獲取鎖作準(zhǔn)備。

    • 如果exclusiveOwnerThread中為當(dāng)前線程說明本身就占有著鎖,現(xiàn)在又嘗試獲取鎖,需要將狀態(tài)變量的值state+1

// ReentrantLock.FairSync.tryAcquire()
protected final boolean tryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();
    // 狀態(tài)變量的值為0,說明暫時還沒有線程占有鎖
    if (c == 0) {
        // hasQueuedPredecessors()保證了不論是新的線程還是已經(jīng)排隊的線程都順序使用鎖
        if (!hasQueuedPredecessors() &&
            compareAndSetState(0, acquires)) {
            // 當(dāng)前線程獲取了鎖,并將本線程設(shè)置到exclusiveOwnerThread變量中,
            //供后續(xù)自己可重入獲取鎖作準(zhǔn)備
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    
    // 之所以說是重入鎖,就是因為在獲取鎖失敗的情況下,還會再次判斷是否當(dāng)前線程已經(jīng)持有鎖了
    else if (current == getExclusiveOwnerThread()) {
        int nextc = c + acquires;
        if (nextc < 0)
            throw new Error("Maximum lock count exceeded");
        // 設(shè)置到state中
        // 因為當(dāng)前線程占有著鎖,其它線程只會CAS把state從0更新成1,是不會成功的
        // 所以不存在競爭,自然不需要使用CAS來更新
        setState(nextc);
        return true;
    }
    return false;
}

如果獲取失敗加入隊列里,那具體怎么處理呢?通過自旋的方式,隊列中線程不斷進(jìn)行嘗試獲取鎖操作,中間是可以通過中斷的方式打斷,

  • 如果當(dāng)前節(jié)點的前一個節(jié)點為head節(jié)點,則說明輪到自己獲取鎖了,調(diào)用tryAcquire()方法再次嘗試獲取鎖

    final boolean acquireQueued(final Node node, int arg) {
        boolean failed = true;
        try {
            boolean interrupted = false;
            // 自旋
            for (;;) {
                // 當(dāng)前節(jié)點的前一個節(jié)點,
                final Node p = node.predecessor();
                // 如果當(dāng)前節(jié)點的前一個節(jié)點為head節(jié)點,則說明輪到自己獲取鎖了
                // 調(diào)用ReentrantLock.FairSync.tryAcquire()方法再次嘗試獲取鎖
                if (p == head && tryAcquire(arg)) {
                    setHead(node);
                    p.next = null; // help GC
                    // 未失敗
                    failed = false;
                    return interrupted;
                }
                // 是否需要阻塞
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
    
            if (failed)
                  // 如果失敗了,取消獲取鎖
                cancelAcquire(node);
        }
    }


  • 當(dāng)前的Node的上一個節(jié)點不是Head,是需要判斷是否需要阻塞,以及尋找安全點掛起。

    private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
        // 上一個節(jié)點的等待狀態(tài)
        int ws = pred.waitStatus;
        // 等待狀態(tài)為SIGNAL(等待喚醒),直接返回true
        if (ws == Node.SIGNAL)
            return true;
        // 前一個節(jié)點的狀態(tài)大于0,已取消狀態(tài)
        if (ws > 0) {
            // 把前面所有取消狀態(tài)的節(jié)點都從鏈表中刪除
            do {
                node.prev = pred = pred.prev;
            } while (pred.waitStatus > 0);
            pred.next = node;
        } else {
            // 前一個Node的狀態(tài)小于等于0,則把其狀態(tài)設(shè)置為等待喚醒
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
        return false;
    }


在看完獲取鎖的流程,那么你知道ReentrantLock如何實現(xiàn)公平鎖了嗎?其實就是在tryAcquire()的實現(xiàn)中。

ReentrantLock如何實現(xiàn)公平鎖?

tryAcquire()的實現(xiàn)中使用了hasQueuedPredecessors()保證了線程先進(jìn)先出FIFO的使用鎖,不會產(chǎn)生"饑餓"問題,

protected final boolean tryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();
    // 狀態(tài)變量的值為0,說明暫時還沒有線程占有鎖
    if (c == 0) {
        // hasQueuedPredecessors()保證了不論是新的線程還是已經(jīng)排隊的線程都順序使用鎖
        if (!hasQueuedPredecessors() &&
            compareAndSetState(0, acquires)) {
          ....
        }
        ...
    }  
}
public final boolean hasQueuedPredecessors() {  
        Node t = tail; 
        Node h = head;
        Node s;
        return h != t &&
            ((s = h.next) == null || s.thread != Thread.currentThread());
    }

tryAcquire都會檢查CLH隊列中是否仍有前驅(qū)的元素,如果仍然有那么繼續(xù)等待,通過這種方式來保證先來先服務(wù)的原則。

那這樣ReentrantLock如何實現(xiàn)可重入?是怎么重入的?

ReentrantLock如何實現(xiàn)可重入?

其實也很簡單,在獲取鎖后,設(shè)置一個標(biāo)識變量為當(dāng)前線程exclusiveOwnerThread,當(dāng)線程再次進(jìn)入判斷exclusiveOwnerThread變量是否等于本線程來判斷.

protected final boolean tryAcquire(int acquires) {
  
    // 狀態(tài)變量的值為0,說明暫時還沒有線程占有鎖
    if (c == 0) {
     	if (!hasQueuedPredecessors() &&
            compareAndSetState(0, acquires)) {
            // 當(dāng)前線程獲取了鎖,并將本線程設(shè)置到exclusiveOwnerThread變量中,
            //供后續(xù)自己可重入獲取鎖作準(zhǔn)備
            setExclusiveOwnerThread(current);
            return true;
        }
    } //之所以說是重入鎖,就是因為在獲取鎖失敗的情況下,還會再次判斷是否當(dāng)前線程已經(jīng)持有鎖了
    else if (current == getExclusiveOwnerThread()) {
        ...
    }
   
}

當(dāng)看完公平鎖獲取鎖的流程,那其實我們也了解非公平鎖獲取鎖,那我們來看看。

ReentrantLock公平鎖模式與非公平鎖獲取鎖的區(qū)別?

其實非公平鎖獲取鎖獲取區(qū)別主要在于:

  • 構(gòu)建函數(shù)中傳入false或者為null,為創(chuàng)建非公平鎖NonfairSync,true創(chuàng)建公平鎖,

  • 非公平鎖在獲取鎖的時候,先去檢查state狀態(tài),再直接執(zhí)行aqcuire(1),這樣可以提高效率,

    final void lock() {
                if (compareAndSetState(0, 1))
                    //修改同步狀態(tài)的值成功的話,設(shè)置當(dāng)前線程為獨占的線程
                    setExclusiveOwnerThread(Thread.currentThread());
                else
                    //獲取鎖
                    acquire(1);
            }


  • tryAcquire()中沒有hasQueuedPredecessors()保證了不論是新的線程還是已經(jīng)排隊的線程都順序使用鎖。

其他功能都類似。在理解了獲取鎖下,我們更好理解ReentrantLock::unlock()鎖的釋放,也比較簡單。

ReentrantLock::unlock()釋放鎖,如何喚醒等待隊列中的線程?

  • 釋放當(dāng)前線程占用的鎖

    protected final boolean tryRelease(int releases) {
        // 計算釋放后state值
        int c = getState() - releases;
        // 如果不是當(dāng)前線程占用鎖,那么拋出異常
        if (Thread.currentThread() != getExclusiveOwnerThread())
            throw new IllegalMonitorStateException();
        boolean free = false;
        if (c == 0) {
            // 鎖被重入次數(shù)為0,表示釋放成功
            free = true;
            // 清空獨占線程
            setExclusiveOwnerThread(null);
        }
        // 更新state值
        setState(c);
        return free;
    }


  • 若釋放成功,就需要喚醒等待隊列中的線程,先查看頭結(jié)點的狀態(tài)是否為SIGNAL,如果是則喚醒頭結(jié)點的下個節(jié)點關(guān)聯(lián)的線程,如果釋放失敗那么返回false表示解鎖失敗。

    • 設(shè)置waitStatus為0,

    • 當(dāng)頭結(jié)點下一個節(jié)點不為空的時候,會直接喚醒該節(jié)點,如果該節(jié)點為空,則會隊尾開始向前遍歷,找到最后一個不為空的節(jié)點,然后喚醒。

private void unparkSuccessor(Node node) {
    int ws = node.waitStatus;
    if (ws < 0)
    compareAndSetWaitStatus(node, ws, 0);
    Node s = node.next;//這里的s是頭節(jié)點(現(xiàn)在是頭節(jié)點持有鎖)的下一個節(jié)點,也就是期望喚醒的節(jié)點
    if (s == null || s.waitStatus > 0) {
        s = null;
        for (Node t = tail; t != null && t != node; t = t.prev)
            if (t.waitStatus <= 0)
        		s = t;
    }
    if (s != null)
    	LockSupport.unpark(s.thread); //喚醒s代表的線程
}

綜合上面的ReentrantLock的可重入,可實現(xiàn)公平\非公平鎖的特性外,還具有哪些特性?

ReentrantLock除了可重入還有哪些特性?

  • 支持線程中斷,只是在線程上增加一個中斷標(biāo)志interrupted,并不會對運行中的線程有什么影響,具體需要根據(jù)這個中斷標(biāo)志干些什么,用戶自己去決定。比如,實現(xiàn)了等待鎖的時候,5秒沒有獲取到鎖,中斷等待,線程繼續(xù)做其它事情。

  • 超時機(jī)制,在ReetrantLock::tryLock(long timeout, TimeUnit unit) 提供了超時獲取鎖的功能。它的語義是在指定的時間內(nèi)如果獲取到鎖就返回true,獲取不到則返回false。這種機(jī)制避免了線程無限期的等待鎖釋放。

ReentrantLock與Synchrionized的區(qū)別

  • ReentrantLock支持等待可中斷,可以中斷等待中的線程

  • ReentrantLock可實現(xiàn)公平鎖

  • ReentrantLock可實現(xiàn)選擇性通知,即可以有多個Condition隊列

ReentrantLock使用場景

  • 場景1:如果已加鎖,則不再重復(fù)加鎖,多用于進(jìn)行非重要任務(wù)防止重復(fù)執(zhí)行,如,清除無用臨時文件,檢查某些資源的可用性,數(shù)據(jù)備份操作等

  • 場景2:如果發(fā)現(xiàn)該操作已經(jīng)在執(zhí)行,則嘗試等待一段時間,等待超時則不執(zhí)行,防止由于資源處理不當(dāng)長時間占用導(dǎo)致死鎖情況

  • 場景3:如果發(fā)現(xiàn)該操作已經(jīng)加鎖,則等待一個一個加鎖,主要用于對資源的爭搶(如:文件操作,同步消息發(fā)送,有狀態(tài)的操作等)

  • 場景4:可中斷鎖,取消正在同步運行的操作,來防止不正常操作長時間占用造成的阻塞

上述就是小編為大家分享的怎么深入理解ReentrantLock原理了,如果剛好有類似的疑惑,不妨參照上述分析進(jìn)行理解。如果想知道更多相關(guān)知識,歡迎關(guān)注億速云行業(yè)資訊頻道。

向AI問一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進(jìn)行舉報,并提供相關(guān)證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI