面試:為了進(jìn)阿里,需要深入理解ReentrantLock原理

在面試肠套,很多時(shí)間面試官都會(huì)問到鎖的問題舰涌,ReentrantLock也是常問一個(gè)點(diǎn),但具體會(huì)問什么呢?在網(wǎng)上收集到一些問題:

  • 重入鎖是什么你稚?

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

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

  • ReentrantLock如何實(shí)現(xiàn)公平鎖刁赖?

  • ReentrantLock如何實(shí)現(xiàn)可重入搁痛?

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

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

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

  • ReentrantLock與Synchrionized的區(qū)別

  • ReentrantLock使用場(chǎng)景

那么重入鎖是什么?有什么用呢

ReentrantLock是什么枪芒?

ReentrantLock是個(gè)典型的獨(dú)占模式AQS彻况,同步狀態(tài)為0時(shí)表示空閑。當(dāng)有線程獲取到空閑的同步狀態(tài)時(shí)舅踪,它會(huì)將同步狀態(tài)加1纽甘,將同步狀態(tài)改為非空閑,于是其他線程掛起等待硫朦。在修改同步狀態(tài)的同時(shí)贷腕,并記錄下自己的線程,作為后續(xù)重入的依據(jù)咬展,即一個(gè)線程持有某個(gè)對(duì)象的鎖時(shí)泽裳,再次去獲取這個(gè)對(duì)象的鎖是可以成功的。如果是不可重入的鎖的話破婆,就會(huì)造成死鎖涮总。

ReentrantLock會(huì)涉及到公平鎖和非公平鎖,實(shí)現(xiàn)關(guān)鍵在于成員變量 sync 的實(shí)現(xiàn)不同,這是鎖實(shí)現(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)鎖可用時(shí),在鎖上等待時(shí)間最長(zhǎng)的線程將獲得鎖的使用權(quán)谤职,即先進(jìn)先出。而非公平鎖則隨機(jī)分配這種使用權(quán)亿鲜,是一種搶占機(jī)制允蜈,是隨機(jī)獲得鎖,并不是先來的一定能先得到鎖蒿柳。

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

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

雖然公平鎖在公平性得以保障,但因?yàn)楣降墨@取鎖沒有考慮到操作系統(tǒng)對(duì)線程的調(diào)度因素以及其他因素垒探,會(huì)影響性能妓蛮。

雖然非公平模式效率比較高,但是非公平模式在申請(qǐng)獲取鎖的線程足夠多,那么可能會(huì)造成某些線程長(zhǎng)時(shí)間得不到鎖圾叼,這就是非公平鎖的“饑餓”問題蛤克。

但大部分情況下我們使用非公平鎖,因?yàn)槠湫阅鼙裙芥i好很多褐奥。但是公平鎖能夠避免線程饑餓咖耘,某些情況下也很有用。

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

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

image

首先需要在構(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) {
   // 嘗試獲取鎖
   // 如果失敗了呜笑,就排隊(duì)
   if (!tryAcquire(arg) &&
       // 注意addWaiter()這里傳入的節(jié)點(diǎn)模式為獨(dú)占模式
       acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
       selfInterrupt();
}

具體獲取鎖流程

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

    image
  • 如果狀態(tài)變量的值為0,說明暫時(shí)還沒有人占有鎖叫胁, 使用hasQueuedPredecessors()保證了不論是新的線程還是已經(jīng)排隊(duì)的線程都順序使用鎖凰慈,如果沒有其它線程在排隊(duì),那么當(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,說明暫時(shí)還沒有線程占有鎖
   if (c == 0) {
       // hasQueuedPredecessors()保證了不論是新的線程還是已經(jīng)排隊(duì)的線程都順序使用鎖
       if (!hasQueuedPredecessors() &&
           compareAndSetState(0, acquires)) {
           // 當(dāng)前線程獲取了鎖买乃,并將本線程設(shè)置到exclusiveOwnerThread變量中姻氨,
           //供后續(xù)自己可重入獲取鎖作準(zhǔn)備
           setExclusiveOwnerThread(current);
           return true;
      }
  }
   
   // 之所以說是重入鎖,就是因?yàn)樵讷@取鎖失敗的情況下剪验,還會(huì)再次判斷是否當(dāng)前線程已經(jīng)持有鎖了
   else if (current == getExclusiveOwnerThread()) {
       int nextc = c + acquires;
       if (nextc < 0)
           throw new Error("Maximum lock count exceeded");
       // 設(shè)置到state中
       // 因?yàn)楫?dāng)前線程占有著鎖肴焊,其它線程只會(huì)CAS把state從0更新成1前联,是不會(huì)成功的
       // 所以不存在競(jìng)爭(zhēng),自然不需要使用CAS來更新
       setState(nextc);
       return true;
  }
   return false;
}

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

  • 如果當(dāng)前節(jié)點(diǎn)的前一個(gè)節(jié)點(diǎn)為head節(jié)點(diǎn),則說明輪到自己獲取鎖了席揽,調(diào)用 tryAcquire() 方法再次嘗試獲取鎖
final boolean acquireQueued(final Node node, int arg) {
   boolean failed = true;
   try {
       boolean interrupted = false;
       // 自旋
       for (;;) {
           // 當(dāng)前節(jié)點(diǎn)的前一個(gè)節(jié)點(diǎn),
           final Node p = node.predecessor();
           // 如果當(dāng)前節(jié)點(diǎn)的前一個(gè)節(jié)點(diǎn)為head節(jié)點(diǎn)谓厘,則說明輪到自己獲取鎖了
           // 調(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的上一個(gè)節(jié)點(diǎn)不是Head,是需要判斷是否需要阻塞,以及尋找安全點(diǎn)掛起竟稳。
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
   // 上一個(gè)節(jié)點(diǎn)的等待狀態(tài)
   int ws = pred.waitStatus;
   // 等待狀態(tài)為SIGNAL(等待喚醒)属桦,直接返回true
   if (ws == Node.SIGNAL)
       return true;
   // 前一個(gè)節(jié)點(diǎn)的狀態(tài)大于0,已取消狀態(tài)
   if (ws > 0) {
       // 把前面所有取消狀態(tài)的節(jié)點(diǎn)都從鏈表中刪除
       do {
           node.prev = pred = pred.prev;
      } while (pred.waitStatus > 0);
       pred.next = node;
  } else {
       // 前一個(gè)Node的狀態(tài)小于等于0他爸,則把其狀態(tài)設(shè)置為等待喚醒
       compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
  }
   return false;
}

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

ReentrantLock如何實(shí)現(xiàn)公平鎖系谐?

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

protected final boolean tryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();
    // 狀態(tài)變量的值為0纪他,說明暫時(shí)還沒有線程占有鎖
    if (c == 0) {
        // hasQueuedPredecessors()保證了不論是新的線程還是已經(jīng)排隊(duì)的線程都順序使用鎖
        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都會(huì)檢查CLH隊(duì)列中是否仍有前驅(qū)的元素,如果仍然有那么繼續(xù)等待晾匠,通過這種方式來保證先來先服務(wù)的原則茶袒。

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

ReentrantLock如何實(shí)現(xiàn)可重入薪寓?

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

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

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

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

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

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

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

final void lock() {
            if (compareAndSetState(0, 1))
                //修改同步狀態(tài)的值成功的話钠至,設(shè)置當(dāng)前線程為獨(dú)占的線程
                setExclusiveOwnerThread(Thread.currentThread());
            else
                //獲取鎖
                acquire(1);
        }
  • tryAcquire() 中沒有 hasQueuedPredecessors() 保證了不論是新的線程還是已經(jīng)排隊(duì)的線程都順序使用鎖。

其他功能都類似胎源。在理解了獲取鎖下棉钧,我們更好理解ReentrantLock::unlock()鎖的釋放,也比較簡(jiǎn)單涕蚤。

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

  • 釋放當(dāng)前線程占用的鎖
protected final boolean tryRelease(int releases) {
    // 計(jì)算釋放后state值
    int c = getState() - releases;
    // 如果不是當(dāng)前線程占用鎖,那么拋出異常
    if (Thread.currentThread() != getExclusiveOwnerThread())
        throw new IllegalMonitorStateException();
    boolean free = false;
    if (c == 0) {
        // 鎖被重入次數(shù)為0,表示釋放成功
        free = true;
        // 清空獨(dú)占線程
        setExclusiveOwnerThread(null);
    }
    // 更新state值
    setState(c);
    return free;
}
  • 若釋放成功万栅,就需要喚醒等待隊(duì)列中的線程 佑钾,先查看頭結(jié)點(diǎn)的狀態(tài)是否為SIGNAL,如果是則喚醒頭結(jié)點(diǎn)的下個(gè)節(jié)點(diǎn)關(guān)聯(lián)的線程烦粒,如果釋放失敗那么返回false表示解鎖失敗休溶。

  • 設(shè)置waitStatus為0,

  • 當(dāng)頭結(jié)點(diǎn)下一個(gè)節(jié)點(diǎn)不為空的時(shí)候扰她,會(huì)直接喚醒該節(jié)點(diǎn)邮偎,如果該節(jié)點(diǎn)為空,則會(huì)隊(duì)尾開始向前遍歷义黎,找到最后一個(gè)不為空的節(jié)點(diǎn)禾进,然后喚醒。

private void unparkSuccessor(Node node) {
    int ws = node.waitStatus;
    if (ws < 0)
    compareAndSetWaitStatus(node, ws, 0);
    Node s = node.next;//這里的s是頭節(jié)點(diǎn)(現(xiàn)在是頭節(jié)點(diǎn)持有鎖)的下一個(gè)節(jié)點(diǎn)廉涕,也就是期望喚醒的節(jié)點(diǎn)
    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的可重入泻云,可實(shí)現(xiàn)公平\非公平鎖的特性外,還具有哪些特性狐蜕?

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

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

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

ReentrantLock與Synchrionized的區(qū)別

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

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

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

ReentrantLock使用場(chǎng)景

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

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

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

  • 場(chǎng)景4:可中斷鎖,取消正在同步運(yùn)行的操作浪慌,來防止不正常操作長(zhǎng)時(shí)間占用造成的阻塞

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末冤荆,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子权纤,更是在濱河造成了極大的恐慌钓简,老刑警劉巖,帶你破解...
    沈念sama閱讀 219,039評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件汹想,死亡現(xiàn)場(chǎng)離奇詭異外邓,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)古掏,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,426評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門损话,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事丧枪」馔浚” “怎么了?”我有些...
    開封第一講書人閱讀 165,417評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵豪诲,是天一觀的道長(zhǎng)顶捷。 經(jīng)常有香客問我,道長(zhǎng)屎篱,這世上最難降的妖魔是什么服赎? 我笑而不...
    開封第一講書人閱讀 58,868評(píng)論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮交播,結(jié)果婚禮上重虑,老公的妹妹穿的比我還像新娘。我一直安慰自己秦士,他們只是感情好缺厉,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,892評(píng)論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著隧土,像睡著了一般提针。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上曹傀,一...
    開封第一講書人閱讀 51,692評(píng)論 1 305
  • 那天辐脖,我揣著相機(jī)與錄音,去河邊找鬼皆愉。 笑死嗜价,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的幕庐。 我是一名探鬼主播久锥,決...
    沈念sama閱讀 40,416評(píng)論 3 419
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼异剥!你這毒婦竟也來了瑟由?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,326評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤冤寿,失蹤者是張志新(化名)和其女友劉穎错妖,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體疚沐,經(jīng)...
    沈念sama閱讀 45,782評(píng)論 1 316
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡暂氯,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,957評(píng)論 3 337
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了亮蛔。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片痴施。...
    茶點(diǎn)故事閱讀 40,102評(píng)論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出辣吃,到底是詐尸還是另有隱情动遭,我是刑警寧澤,帶...
    沈念sama閱讀 35,790評(píng)論 5 346
  • 正文 年R本政府宣布神得,位于F島的核電站厘惦,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏哩簿。R本人自食惡果不足惜宵蕉,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,442評(píng)論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望节榜。 院中可真熱鬧羡玛,春花似錦、人聲如沸宗苍。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,996評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽讳窟。三九已至让歼,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間丽啡,已是汗流浹背谋右。 一陣腳步聲響...
    開封第一講書人閱讀 33,113評(píng)論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留碌上,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,332評(píng)論 3 373
  • 正文 我出身青樓浦徊,卻偏偏與公主長(zhǎng)得像馏予,于是被迫代替她去往敵國和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子盔性,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,044評(píng)論 2 355