ReentrantLock 源碼分析

1、ReentrantLock特性

??ReentrantLock是一把重入鎖脖含,可中斷豁翎,可以限時角骤,支持公平鎖和非公平鎖。
??下面舉一個生活中的例子心剥,幫助大家來更好的理解ReentrantLock這些特性邦尊。
??快過年了,在北上廣的小伙伴們紛紛踏上了回家的旅途优烧。由于小伙伴們一年都沒回家了蝉揍,一下班拿起行李箱就奔向了火車站。爭先恐后的跑到G1020檢票口檢票回家畦娄,誰先跑到檢票口又沾,誰先驗票,誰先回家熙卡。那些跑的慢沒有搶到檢票權(quán)的杖刷,一個一個的在后面排成一隊,先到的排在前面驳癌。在最前面獲得檢票權(quán)的人通過檢票機驗票走人,檢票機通知下面一個人來驗票滑燃。這樣每次最前面的人驗票走人,然后依次下一個颓鲜。這樣一來大家都井然有序的回家表窘,心里面也比較平衡典予,誰讓自己腿短跑的慢呢,排在跑的快的人后面也心安理得乐严。這就是公平鎖的基本思路熙参。
??在看G1028檢票口的人,這批人就比較聰明了麦备,看著前面一眼望不到頭的長長的隊伍孽椰。把自己的行李箱放在隊伍中代替自己,自己則在旁邊的椅子上歇著凛篙。沒辦法誰讓做這趟車的人聰明呢黍匾,這下可把檢票機給累壞了,每次檢查到行李箱到就大聲喊呛梆,黑色行李箱的來安檢了锐涯,然后聽到的人慢悠悠的在過去驗票走人。后來智能的檢票機通過智能學(xué)習(xí)也變聰明了填物,如果在檢查到放行李箱占位置的纹腌,如果此時剛好有人過來檢票,則直接讓此人檢票滞磺,不需要在隊伍尾部排隊等候了升薯。這樣雖然對搶到前面的人不公平夏跷,但是卻加快了進站效率规伐。這就是非公平鎖的基本思路。
??現(xiàn)在檢票的規(guī)則變了典鸡,以家庭為單位檢票阅茶,只要是家庭中的一員搶到檢票口蛛枚,其余人就可以跟著過檢票口。由于小明跑的快脸哀,先跑到了檢票口蹦浦。他的父母年齡比較大,跑的比較慢撞蜂。等跑到檢票口的時候盲镶,后面已經(jīng)排起來了長隊×律悖可是人家有一個跑的快的兒子呀徒河,現(xiàn)在的規(guī)則又是以家庭為單位,都是一家人送漠,于是小明的父母也可以直接檢票走人顽照,不需要去隊伍里面排隊。這就是可重入鎖,同一個線程可以重復(fù)拿鎖代兵。
??快看G1028檢票口的人吵起來了尼酿,怎么回事呢。原來是禿頭和長毛兩個人剛才跑的快撞在了一起植影,身份ID都掉在了地上裳擎,2個人匆忙撿起來就跑到了檢票口。禿頭跑的快一點思币,跑到了長毛的前面鹿响。這時候禿頭拿身份id驗證的時候發(fā)現(xiàn)身份不對,上面寫的是產(chǎn)品經(jīng)理谷饿。后面那個人拿的是禿頭的身份ID惶我,上面寫的程序員。于是禿頭說,我把你的身份ID給你博投,你把我的身份ID給我绸贡。但是長毛平時提需求提的習(xí)慣了,對禿頭說給你可以毅哗,但是讓我排到你前面听怕。禿頭一聽這無腦需求,火冒三丈虑绵,就和長毛干了起來尿瞭。這下好了,長毛不給禿頭身份ID蒸殿,禿頭也不給長毛身份ID筷厘,兩個人就互相僵持著鸣峭。這導(dǎo)致后面排隊的人也沒發(fā)進站了宏所。這時候長毛手機發(fā)生了異常,一個電話打了過來摊溶,原來老板讓長毛回去改需求爬骤,沒辦法最后中斷了2人的爭執(zhí)。長毛灰溜溜的走了莫换,禿頭打贏了這場仗霞玄,臉上露出了陽光般的笑容。這就是可中斷的拉岁,當(dāng)2個線程互相占有鎖坷剧,不釋放導(dǎo)致死鎖的時候,ReentrantLock可以用鎖中斷解決喊暖。
??在看另一邊一個人想要插隊惫企,在耐心的說服前面的人,讓我先進去吧,讓我先進去吧狞尔〈园妫可是說服了幾分鐘也沒說服成功,于是放棄了不在插隊搶先檢票了偏序。這個就是在一定時間內(nèi)鎖嘗試,嘗試著去獲取鎖页畦,如果沒有獲取到就結(jié)束。
??以上就是重入鎖研儒,鎖中斷豫缨,鎖限時,公平鎖和非公平鎖的大致概念端朵,相信大家應(yīng)該會有所理解州胳。下面對ReentrantLock的特性進行詳細的講解。

2逸月、ReentrantLock非公平鎖

ReentrantLock默認實現(xiàn)的是非公平鎖栓撞,我具體看一下代碼:

ReentrantLock reentrantLock = new ReentrantLock();
reentrantLock.lock();

我們看一下構(gòu)造函數(shù):

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

從這里可以看出ReentrantLock默認實現(xiàn)的是非公平鎖,我們在看一下非公平鎖是怎么具體實現(xiàn)的碗硬。
lock是一個接口,構(gòu)造器默認實現(xiàn)的是NonfairSync瓤湘,所reentrantLock.lock() 調(diào)用的是NonfairSync的lock接口。

看一下這塊的具體代碼:

static final class NonfairSync extends Sync {
    private static final long serialVersionUID = 7316153563782823691L;

    /**
     * Performs lock.  Try immediate barge, backing up to normal
     * acquire on failure.
     */
    final void lock() {
        if (compareAndSetState(0, 1))
            setExclusiveOwnerThread(Thread.currentThread());
        else
            acquire(1);
    }

    protected final boolean tryAcquire(int acquires) {
        return nonfairTryAcquire(acquires);
    }
}

??NonfairSync 繼承了Sync,Sync繼承了AbstractQueuedSynchronizer恩尾,到這里大家應(yīng)該已經(jīng)明白ReentrantLock是基于AQS實現(xiàn)的弛说,所以只要你搞懂AQS,很多并發(fā)類你都會很容易的理解翰意。

??reentrantLock.lock()木人,我們看一下lock方法做了哪些操作,首先通過CAS獲取鎖冀偶,如果獲取到鎖醒第,把當(dāng)前線程設(shè)置為獨占線程。如果獲取失敗进鸠,則調(diào)用acquire方法稠曼,而此方法為AQS內(nèi)部方法,此處不在詳細的展開分析客年。

public final void acquire(int arg) {
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}

??這里有一個知識點霞幅,我們看一下NonfairSync類中tryAcquire方法,此方法也是AQS中的方法量瓜,也就是子類NonfairSync重寫了父類AQS的tryAcquire方法司恳。
??當(dāng)子類NonfairSync調(diào)用acquire方法的時候,執(zhí)行的是AQS提供的acquire方法绍傲,然后從上面代碼中可以看出來父類AQS在此方法中執(zhí)行了tryAcquire方法扔傅,而tryAcquire方法在子類中已經(jīng)重寫,那么就會執(zhí)行子類NonfairSync實現(xiàn)的tryAcquire方法。這就是多態(tài)铅鲤。
??這也是AQS的好處划提,對外提供API,子類繼承AQS邢享,按照自己的業(yè)務(wù)邏輯重寫提供的API鹏往。而AQS只管線程怎么進行入隊列,怎么插入節(jié)點骇塘,怎么喚醒節(jié)點這些底層的方法伊履,對外層提供調(diào)用的 API,然后子類只需要繼承AQS款违,實現(xiàn)獨有的業(yè)務(wù)方法即可唐瀑,從而大大降低了耦合度。

我們在看一下nonfairTryAcquire方法做了哪寫操作:

final boolean nonfairTryAcquire(int acquires) {
      final Thread current = Thread.currentThread();//獲取當(dāng)前線程
      int c = getState();//獲取當(dāng)前線程的狀態(tài)
      if (c == 0) {//如果當(dāng)前線程處于初始狀態(tài)
          if (compareAndSetState(0, acquires)) {//cas競爭鎖
              setExclusiveOwnerThread(current);//競爭到鎖把當(dāng)前線程設(shè)置為獨占線程
              return true;
          }
      }
      else if (current == getExclusiveOwnerThread()) {//如果當(dāng)前線程是獨占線程插爹,注意此方法也是實現(xiàn)重入的地方哄辣。
          int nextc = c + acquires;//當(dāng)前線程狀態(tài)值+傳入的值
          if (nextc < 0) // overflow
              throw new Error("Maximum lock count exceeded");
          setState(nextc);//設(shè)置最新的狀態(tài)值
          return true;
      }
      return false;
  }

??首先取到當(dāng)前線程和當(dāng)前線程的值,如果當(dāng)前線程是初始狀態(tài)那么就去競爭鎖赠尾,如果競爭到鎖力穗,把當(dāng)前線程設(shè)置為獨占線程。如果進來的線程是獨占線程气嫁,那么更新此線程進入的次數(shù)当窗,同時也可以獲取鎖。如果沒有競爭到鎖寸宵,也不是當(dāng)前的獨占線程崖面,那么就返回false。

??從此方法中可以看出來梯影,只要有線程進來巫员,就讓他獲取鎖,而不是排隊到尾部光酣。只要是獨占線程疏遏,就可以重復(fù)進來,正是通過此方法可以看出來ReentrantLock是可以進行重入的也是可以是實現(xiàn)非公平鎖的救军。

3、ReentrantLock公平鎖

我們在看一下ReentrantLock實現(xiàn)的公平鎖的源代碼:

ReentrantLock reentrantLock=new ReentrantLock(true)倘零;
reentrantLock.lock();

我們點擊構(gòu)造器看一下:

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

下面具體看一下FairSync類的源代碼:

static final class FairSync extends Sync {
    private static final long serialVersionUID = -3000897897090466540L;

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

    /**
     * Fair version of tryAcquire.  Don't grant access unless
     * recursive call or no waiters or is first.
     */
    protected final boolean tryAcquire(int acquires) {
        final Thread current = Thread.currentThread();
        int c = getState();
        if (c == 0) {
          //沒有線程在等隊列里面等待同時獲取到鎖唱遭,則設(shè)置當(dāng)前線程為獨占線程
            if (!hasQueuedPredecessors() &&
                compareAndSetState(0, acquires)) {
                setExclusiveOwnerThread(current);
                return true;
            }
        }
        else if (current == getExclusiveOwnerThread()) {
            int nextc = c + acquires;
            if (nextc < 0)
                throw new Error("Maximum lock count exceeded");
            setState(nextc);
            return true;
        }
        return false;
    }
}

??FairSync也是繼承了Sync,也就是也是繼承了AQS呈驶,調(diào)用lock方法拷泽,lock方法調(diào)用了父類acquire,此方法會調(diào)用子類重寫的tryAcquire方法。它的實現(xiàn)方式和上面講的非公平鎖實現(xiàn)方式大致一樣司致,業(yè)務(wù)邏輯都是在重寫的tryAcquire里面拆吆。
我們看一下hasQueuedPredecessors:

public final boolean hasQueuedPredecessors() {
    // The correctness of this depends on head being initialized
    // before tail and on head.next being accurate if the current
    // thread is first in queue.
    Node t = tail; // Read fields in reverse initialization order
    Node h = head;
    Node s;
    return h != t &&
        ((s = h.next) == null || s.thread != Thread.currentThread());
}

此方法主要是查看是否有線程在等待隊列里面等待。

??FairSync類tryAcquire方法業(yè)務(wù)邏輯就是獲取到當(dāng)前線程和當(dāng)前線程的狀態(tài)脂矫,如果當(dāng)前線程是初始狀態(tài)枣耀,會去判斷當(dāng)前隊列里面是否有等待的線程,如果隊列中沒有等待的線程庭再,同時獲取到鎖捞奕,那么就把當(dāng)前線程設(shè)置為獨占線程。如果是相同的獨占線程進來拄轻,則更新獨占線程進來的次數(shù)颅围。同時返回true,否則返回false恨搓。從這里可以很容易的看出來院促,這是一個公平鎖,進來的線程需要排隊斧抱,隊列中沒有了線程才能輪到進來的線程一疯。同時在else if 這個條件中可以看出來也是可重入的。

3夺姑、ReentrantLock鎖中斷

我們看一下可中斷鎖的源代碼:

ReentrantLock reentrantLock=new ReentrantLock();
reentrantLock.lockInterruptibly();
public void lockInterruptibly() throws InterruptedException {
    sync.acquireInterruptibly(1);//調(diào)用AQS中方法
}
public final void acquireInterruptibly(int arg)
        throws InterruptedException {
    if (Thread.interrupted())//線程中斷拋出異常
        throw new InterruptedException();
    if (!tryAcquire(arg))//此處還是調(diào)用創(chuàng)建對象子類的方法,獲取不到鎖執(zhí)行下面的方法
        doAcquireInterruptibly(arg);
}
private void doAcquireInterruptibly(int arg)
    throws InterruptedException {
    final Node node = addWaiter(Node.EXCLUSIVE);//封裝線程節(jié)點,并且添加到尾部墩邀。前面文章已經(jīng)詳細講解過,此處不在詳細展開盏浙。
    boolean failed = true;
    try {
        for (;;) {
            final Node p = node.predecessor();//獲取當(dāng)前節(jié)點的前驅(qū)節(jié)點
            if (p == head && tryAcquire(arg)) {//前驅(qū)節(jié)點是頭節(jié)點并且獲取到鎖
                setHead(node);//設(shè)置當(dāng)前節(jié)點為頭節(jié)點
                p.next = null; // help GC
                failed = false;
                return;
            }
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())//線程如果是阻塞的并且被中斷眉睹,則直接拋出異常
                throw new InterruptedException();
        }
    } finally {
        if (failed)
            cancelAcquire(node);//如果線程拋出了異常,那么就把線程狀態(tài)設(shè)置為取消狀態(tài)同時清除節(jié)點.
    }
}

??此處講解一下废膘,為什么ReentrantLock可以進行鎖中斷竹海,為什么可以在產(chǎn)生死鎖的時候,可以通過鎖中斷技術(shù)解決死鎖丐黄≌洌看過源碼其實已經(jīng)明白,首先在當(dāng)前線程如果調(diào)用了interrupted灌闺,那么直接拋出異常出來艰争。如果線程被阻塞并且被中斷了那么也是拋出異常。也就是他是通過線程調(diào)用中斷方法拋出異常來打破持有鎖的桂对。如果前面的文章看過甩卓,你會發(fā)在AQS中doAcquireInterruptibly方法和acquireQueued方法很相似,區(qū)別就是一個是返回boolean類型的值蕉斜,讓上層做判斷逾柿,一個是在返回boolean類型值的地方直接拋出了異常缀棍。

3、ReentrantLock鎖限時

我們看一下可中斷鎖的源代碼:

ReentrantLock reentrantLock=new ReentrantLock();
reentrantLock.tryLock(300, TimeUnit.SECONDS);//300秒內(nèi)持續(xù)獲取鎖,直到獲取到鎖或者時間截止
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);//首先獲取一次鎖机错,如果沒有獲取到執(zhí)行獨占計時模式
}
private boolean doAcquireNanos(int arg, long nanosTimeout)
        throws InterruptedException {
    if (nanosTimeout <= 0L)//時間小于0直接返回
        return false;
    final long deadline = System.nanoTime() + nanosTimeout;//隊列延遲時間為系統(tǒng)時間+設(shè)置的超時時間
    final Node node = addWaiter(Node.EXCLUSIVE);//把當(dāng)前線程封裝為Node并添加到隊列
    boolean failed = true;
    try {
        for (;;) {//自旋
            final Node p = node.predecessor();//獲取當(dāng)前節(jié)點的前驅(qū)節(jié)點
            if (p == head && tryAcquire(arg)) {//如果前驅(qū)節(jié)點是頭節(jié)點并且獲取到鎖
                setHead(node);//設(shè)置當(dāng)前節(jié)點為頭節(jié)點
                p.next = null; // help GC
                failed = false;
                return true;
            }
            nanosTimeout = deadline - System.nanoTime();//超時間為延遲時間-當(dāng)前系統(tǒng)的時間
            if (nanosTimeout <= 0L)//表示已經(jīng)超過設(shè)置的嘗試時間爬范,直接返回
                return false;
            if (shouldParkAfterFailedAcquire(p, node) &&
                nanosTimeout > spinForTimeoutThreshold)//如果當(dāng)前線程阻塞,并且超時時間大于1000納秒
                LockSupport.parkNanos(this, nanosTimeout);//阻塞當(dāng)前線程并在超時時間內(nèi)返回
            if (Thread.interrupted())//線程中斷
                throw new InterruptedException();//拋出異常
        }
    } finally {
        if (failed)//線程發(fā)生異常
            cancelAcquire(node);//把當(dāng)前線程設(shè)置為取消狀態(tài)并清除該節(jié)點
    }
}

通過閱讀源碼我們發(fā)現(xiàn)鎖限時獲取的步驟:

1 首先調(diào)用tryAcquire方法獲取一次鎖,如果沒有獲取到調(diào)用AQS中的doAcquireNanos弱匪。

2 System.nanoTime() 獲取系統(tǒng)納秒級時間+傳遞的延時時間為最后的時間青瀑。

3 調(diào)用addWaiter方法把當(dāng)前線程封裝為節(jié)點并添加到隊列的尾部。

4 前驅(qū)節(jié)點是頭節(jié)點并且獲取到鎖設(shè)置當(dāng)前節(jié)點為頭節(jié)點

5 如果當(dāng)前線程被阻塞了并且超時時間大于1000納秒痢法,調(diào)用LockSupport.parkNanos方法阻塞當(dāng)前線程并且在規(guī)定的超時間內(nèi)返回

6 如果線程中斷狱窘,直接拋出異常,這里可以看出支持鎖中斷

7 如果線程在自旋的過程中發(fā)生了異常财搁,那么調(diào)用cancelAcquire方法把當(dāng)前線程設(shè)置為取消狀態(tài)并且清除該節(jié)點蘸炸。

??其實此方法和上一篇講解的獨占鎖模式調(diào)用acquireQueued方法差不多。不同點在于這里增加了超時時間尖奔,如果超時時間大于spinForTimeoutThreshold搭儒,此值是一個常量為1000的值。也就是如果超時時間大于1000納秒提茁,那么就調(diào)用 LockSupport.parkNanos方法讓該線程阻塞淹禾,最長阻塞的時間不會超過超時的時間。同時增加了線程中斷的判斷茴扁,發(fā)生線程中斷則拋出異常铃岔,其余和acquireQueued實現(xiàn)都一樣。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末峭火,一起剝皮案震驚了整個濱河市毁习,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌卖丸,老刑警劉巖纺且,帶你破解...
    沈念sama閱讀 221,198評論 6 514
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異稍浆,居然都是意外死亡载碌,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,334評論 3 398
  • 文/潘曉璐 我一進店門衅枫,熙熙樓的掌柜王于貴愁眉苦臉地迎上來嫁艇,“玉大人,你說我怎么就攤上這事为鳄∩哑停” “怎么了?”我有些...
    開封第一講書人閱讀 167,643評論 0 360
  • 文/不壞的土叔 我叫張陵孤钦,是天一觀的道長歧斟。 經(jīng)常有香客問我,道長偏形,這世上最難降的妖魔是什么静袖? 我笑而不...
    開封第一講書人閱讀 59,495評論 1 296
  • 正文 為了忘掉前任,我火速辦了婚禮俊扭,結(jié)果婚禮上队橙,老公的妹妹穿的比我還像新娘。我一直安慰自己萨惑,他們只是感情好捐康,可當(dāng)我...
    茶點故事閱讀 68,502評論 6 397
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著庸蔼,像睡著了一般解总。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上姐仅,一...
    開封第一講書人閱讀 52,156評論 1 308
  • 那天花枫,我揣著相機與錄音,去河邊找鬼掏膏。 笑死劳翰,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的馒疹。 我是一名探鬼主播佳簸,決...
    沈念sama閱讀 40,743評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼颖变!你這毒婦竟也來了生均?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,659評論 0 276
  • 序言:老撾萬榮一對情侶失蹤悼做,失蹤者是張志新(化名)和其女友劉穎疯特,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體肛走,經(jīng)...
    沈念sama閱讀 46,200評論 1 319
  • 正文 獨居荒郊野嶺守林人離奇死亡漓雅,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,282評論 3 340
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了朽色。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片邻吞。...
    茶點故事閱讀 40,424評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖葫男,靈堂內(nèi)的尸體忽然破棺而出抱冷,到底是詐尸還是另有隱情,我是刑警寧澤梢褐,帶...
    沈念sama閱讀 36,107評論 5 349
  • 正文 年R本政府宣布旺遮,位于F島的核電站赵讯,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏耿眉。R本人自食惡果不足惜边翼,卻給世界環(huán)境...
    茶點故事閱讀 41,789評論 3 333
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望鸣剪。 院中可真熱鬧组底,春花似錦、人聲如沸筐骇。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,264評論 0 23
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽铛纬。三九已至厌均,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間饺鹃,已是汗流浹背莫秆。 一陣腳步聲響...
    開封第一講書人閱讀 33,390評論 1 271
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留悔详,地道東北人镊屎。 一個月前我還...
    沈念sama閱讀 48,798評論 3 376
  • 正文 我出身青樓,卻偏偏與公主長得像茄螃,于是被迫代替她去往敵國和親缝驳。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,435評論 2 359

推薦閱讀更多精彩內(nèi)容