談談JUC----------ReentrantLock源碼分析

1、ReentrantLock介紹

ReentrantLock是一個和synchronized擁有相同語義但同時擴展了額外功能的可重入互斥鎖實現。ReentrantLock將由最近成功得到鎖定,而且尚未釋放該鎖定的線程所擁有。當鎖定沒有被另外一個線程所擁有時,調用 lock() 的線程將成功獲取該鎖定並返回。若是當前線程已經擁有該鎖定,此方法將當即返回。可使用isHeldByCurrentThread()getHoldCount() 方法來檢查此狀況是否發生。node

ReentrantLock有公平鎖和非公平鎖兩種,經過構造器傳入一個boolean fair參數指定,該參數是可選的,默認爲false,也就是說,默認是非公平鎖實現。但請注意,這裏所說的公平與非公平,只是說獲取鎖的時候是否順序進行,並不保證線程調度的公平性。所以,使用公平鎖的多個線程中的一個可能會連續屢次得到它。公平鎖即在線程相互爭用鎖的狀況下,它會更偏向於讓等待時間最長的那個線程得到鎖(隊頭),但相比較非公平鎖,使用多個線程訪問公平鎖的程序吞吐量比較低,或者明顯更慢。bash

一般狀況下建議將釋放鎖的操做放置在finally{}語句塊中,以下面代碼:併發

public void m() {
   lock.lock();  // block until condition holds
   try {
   // ... method body
   } finally {
        lock.unlock()
  }
 }
複製代碼

2、源碼分析

查看ReentrantLock源碼,發現該類只有一個Sync的成員變量:jvm

private final Sync sync;
複製代碼

Sync爲繼承自AQS的一個同步器實現,其內部同時提供了一個lock()抽象方法供子類實現,完成獲取鎖操做。SyncFairSync,NonfairSync兩個子類,分別提供公平鎖和非公平鎖的相關操做。高併發

abstract static class Sync extends AbstractQueuedSynchronizer {
    abstract void lock();
}
static final class FairSync extends Sync {}
static final class NonfairSync extends Sync {}
複製代碼

下面分別從公平和非公平兩種實現探討其獲取鎖和釋放鎖的操做:源碼分析

一、公平鎖如何獲取鎖?
final void lock() {
    acquire(1);
}
複製代碼

能夠看出其直接調用AQSacquire(int)方法獲取鎖,接下來看下acquire()實現:性能

public final void acquire(int arg) {
    // 一、首先嚐試獲取鎖,若是獲取失敗,那麼就假如等待隊列中
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}
複製代碼

接着查看FairSync中提供的tryAcquire(int)方法:優化

protected final boolean tryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();
    // 一、若是鎖尚未被別人獲取,及同步狀態爲0
    if (c == 0) {
        // 二、判斷是否已經有其餘線程在等待獲取該鎖,若是沒有
        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;
}
複製代碼

從上面代碼第2步能夠看出,公平鎖會偏向於給隊列中等待時間最長的線程優先得到鎖,若是此時沒有其餘線程在等待,則執行CAS爭搶鎖資源。第3步若是該鎖已經被持有,則判斷持有該鎖的線程是否當前線程自己,若是是,那麼同步狀態state遞增(加鎖次數)。能夠看出,公平鎖按等待隊列順序分配鎖資源,高併發下性能,效率不高。ui

二、非公平鎖如何獲取鎖?
final void lock() {
    // 一、直接參與競爭,若是該鎖已被其它線程持有,那麼就執行else
    if (compareAndSetState(0, 1))
        setExclusiveOwnerThread(Thread.currentThread());
    else
        acquire(1);
}
複製代碼

接着繼續查看Sync提供的nonfairTryAcquire()方法,源碼以下:spa

final boolean nonfairTryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();
    if (c == 0) {
        // 一、無需額外判斷是否有其它線程處於等待狀態
        if (compareAndSetState(0, acquires)) {
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    else if (current == getExclusiveOwnerThread()) {
        int nextc = c + acquires;
        if (nextc < 0) // overflow
            throw new Error("Maximum lock count exceeded");
        setState(nextc);
        return true;
    }
    return false;
}
複製代碼

忽然,小編髮現,這個nonfairTryAcquire()方法怎麼和上面講到的tryAcquire(int)有點類似,原來tryAcquire(int)nonfairTryAcquire()多了一步判斷同步隊列中是否有其它線程正在等待,所以,這也是公平和非公平的區別所在,若是nonfairTryAcquire()方法也沒能獲取鎖,那麼將被掛到同步等待隊列中。

那麼,咱們可能會問,既然會被掛到同步隊列中,那當前被掛起的這個線程後續是怎麼被喚醒搶奪鎖資源的呢?仍是按順序出隊列嗎?若是仍是按順序出隊列,是否是就和公平鎖同樣了呢?其實很簡單,仍是回到下面這個方法:

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

首先執行tryAcquire(arg)獲取鎖失敗以後,會執行addWaiter()將本身封裝成Node節點入隊,接着調用acquireQueued()方法,答案就在acquireQueued()方法中,源碼以下:

final boolean acquireQueued(final Node node, int arg) {
    boolean failed = true;
    try {
        boolean interrupted = false;
        // 一、自旋,其餘參與同一個鎖競爭的線程也在同時不斷自旋
        for (;;) {
            final Node p = node.predecessor();
            // 二、不斷嘗試獲取鎖資源
            if (p == head && tryAcquire(arg)) {
                setHead(node);
                p.next = null; // help GC
                failed = false;
                return interrupted;
            }
            // 三、處理中斷狀況
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        // 因爲中斷或者超時,必須將狀態改爲cancel
        if (failed)
            cancelAcquire(node);
    }
}
複製代碼

原來非公平鎖參與鎖競爭失敗、被掛到等待隊列以後,會cas+自旋直到獲取鎖成功,確實很不公平,哈哈。

三、鎖的釋放
public void unlock() {
    sync.release(1);
}
複製代碼

ReentrantLock中釋放鎖統一爲unlock()方法,從上面源碼能夠看出,每調用一次unlock()方法,同步狀態就會減一,也就是說,lock()多少次,就要對應unlock()多少次。

接着深刻release()方法,源碼以下:

public final boolean release(int arg) {
    // 一、嘗試釋放鎖
    if (tryRelease(arg)) {
        Node h = head;
        // 二、喚醒那些因爲中斷或其它狀況致使waitStatus不爲0的節點
        if (h != null && h.waitStatus != 0)
            unparkSuccessor(h);
        return true;
    }
    return false;
}
複製代碼
protected final boolean tryRelease(int releases) {
    int c = getState() - releases;
    // 若是線程不是當前線程持有,那麼就報錯
    if (Thread.currentThread() != getExclusiveOwnerThread())
        throw new IllegalMonitorStateException();
    boolean free = false;
    // 該鎖已經沒有線程持有了,同步狀態爲0了,那麼其它線程就能夠從cas+自旋中退出了
    if (c == 0) {
        free = true;
        setExclusiveOwnerThread(null);
    }
    setState(c);
    return free;
}
複製代碼

3、ReentrantLock與synchronized的區別

都說synchronized使用的是重量級鎖,性能很是低下,可是新版本的jdk已經作了如偏向鎖之類的優化,性能其實還能夠。synchronized屬於jvm層面的加鎖機制,而Lock屬於API層面上的加鎖,那麼它們到底有什麼區別呢?

  • 一、如等待可中斷

    持有鎖的線程若是長期不釋放鎖,正在等待的線程能夠選擇放棄等待。

    1.設置超時方法tryLock(long timeout, TimeUnit unit),時間過了就放棄等待;

    2.調用lockInterruptibly()方法,若是線程中斷了,則結束獲取鎖操做;

  • 二、synchronized爲非公平鎖,ReentrantLock同時支持公平鎖和非公平鎖

  • 三、ReentrantLock可結合Condition條件進行使用,可分別對多種條件加鎖,對線程的等待、喚醒操做更加詳細和靈活,在多個條件變量和高度競爭鎖的地方,ReentrantLock更加適合

有一點須要注意就是,釋放鎖的操做必定要在finally塊執行,不然可能出現死鎖等意外狀況。

4、應用場景

參考文章:

ReentrantLock使用場景和實例

ReentrantLock使用場景以及注意事項

相關文章
相關標籤/搜索