全網最詳細的ReentrantReadWriteLock源碼剖析(萬字長文)


碎碎念)
花了兩天時間,終於把ReentrantReadWriteLock(讀寫鎖)解析做完了。之前鑽研過AQS(AbstractQueuedSynchronizer)的源碼,發現弄懂讀寫鎖也沒有想象中那么困難。而且閱讀完ReentrantReadWriteLock的源碼,正好可以和AQS的源碼串起來理解,相輔相成
AQS的鏈接貼在下方👇👇👇
全網最詳細的AbstractQueuedSynchronizer(AQS)源碼剖析(一)AQS基礎
全網最詳細的AbstractQueuedSynchronizer(AQS)源碼剖析(二)資源的獲取和釋放
全網最詳細的AbstractQueuedSynchronizer(AQS)源碼剖析(三)條件變量

簡介

ReentrantReadWriteLock是一個可重入讀寫鎖,內部提供了讀鎖寫鎖的單獨實現。其中讀鎖用於只讀操作,可被多個線程共享;寫鎖用於寫操作,只能互斥訪問

ReentrantReadWriteLock尤其適合讀多寫少的應用場景

讀多寫少:
在一些業務場景中,大部分只是讀數據,寫數據很少,如果這種場景下依然使用獨占鎖(如synchronized),會大大降低性能。因為獨占鎖會使得本該並行執行的讀操作,變成了串行執行

ReentrantReadWriteLock實現了ReadWriteLock接口,該接口只有兩個方法,分別用於返回讀鎖和寫鎖,這兩個鎖都是Lock對象。該接口源碼如下:

public interface ReadWriteLock {
    Lock readLock();
    Lock writeLock();
}

ReentrantReadWriteLock有兩個域,分別存放讀鎖和寫鎖:

private final ReentrantReadWriteLock.ReadLock readerLock;

private final ReentrantReadWriteLock.WriteLock writerLock;

ReentrantReadWriteLock的核心原理主要在於兩點:

  • 內部類Sync:實現了的AQS大部分方法。Sync類有兩個子類FairSyncNonfairSync,分別實現了公平讀寫鎖非公平讀寫鎖Sync類及其子類的源碼解析會在后面給出
  • 內部類ReadLockWriteLock:分別是讀鎖寫鎖的具體實現,它們都和ReentrantLock一樣實現了Lock接口,因此實現的手段也和ReentrantLock一樣,都是委托給內部的Sync類對象來實現,對應的源碼解析也會在后面給出

說什么Sync類、ReadLockWriteLock類啥的都太抽象,不如一張圖來得實在!ReentrantReadWriteLock和這些內部類的繼承、聚合關系如下圖所示:

作者: 酒冽        出處: https://www.cnblogs.com/frankiedyz/p/15655865.html
版權:本文版權歸作者和博客園共有
轉載:歡迎轉載,但未經作者同意,必須保留此段聲明;必須在文章中給出原文連接;否則 必究法律責任

ReentrantReadWriteLock的特點

讀寫鎖的互斥關系

  • 讀鎖和寫鎖之間是互斥關系:當有線程持有讀鎖時,寫鎖不能獲得;當有其他線程持有寫鎖時,讀鎖不能獲得
  • 讀鎖和讀鎖之間是共享關系
  • 寫鎖和寫鎖之間是互斥關系

可重入性

ReentrantReadWriteLockReadWriteLock接口之上,添加了可重入的特性,且讀鎖和寫鎖都支持可重入。可重入的含義是:

  • 如果一個線程獲取了讀鎖,那么它可以再次獲取讀鎖(但直接獲取寫鎖會失敗,原因見下方的“鎖的升降級”
  • 如果一個線程獲取了寫鎖,那么它可以再次獲取寫鎖或讀鎖

鎖的升降級

鎖升級

ReentrantReadWriteLock不支持鎖升級,即同一個線程獲取讀鎖后,直接申請寫鎖是不能獲取成功的。測試代碼如下:

public class Test1 {
    public static void main(String[] args) {
        ReentrantReadWriteLock rtLock = new ReentrantReadWriteLock();
        rtLock.readLock().lock();
        System.out.println("get readLock.");
        rtLock.writeLock().lock();
        System.out.println("blocking");
    }
}

運行到第6行會因為獲取失敗而被阻塞,導致Test1發生死鎖。命令行輸出如下:

get readLock.

鎖降級

ReentrantReadWriteLock支持鎖降級,即同一個線程獲取寫鎖后,直接申請讀鎖是可以直接成功的。測試代碼如下:

public class Test2 {
    public static void main(String[] args) {
        ReentrantReadWriteLock rtLock = new ReentrantReadWriteLock();
        rtLock.writeLock().lock();
        System.out.println("writeLock");
        rtLock.readLock().lock();
        System.out.println("get read lock");
    }
}

該程序不會產生死鎖。結果輸出如下:

writeLock
get read lock

Process finished with exit code 0

讀寫鎖的升降級規則總結

  • ReentrantReadWriteLock不支持鎖升級,因為可能有其他線程同時持有讀鎖,而讀寫鎖之間是互斥的,因此升級為寫鎖存在沖突
  • ReentrantReadWriteLock支持鎖降級,因為如果該線程持有寫鎖時,一定沒有其他線程持有讀鎖或寫鎖,因此降級為讀鎖不存在沖突

公平鎖和非公平鎖

ReentrantReadWriteLock支持公平模式和非公平模式獲取鎖。從性能上來看,非公平模式更好

二者的規則如下:

  • 公平鎖:無論是讀線程還是寫線程,在申請鎖時都會檢查是否有其他線程在同步隊列中等待。如果有,則讓步
  • 非公平鎖:如果是讀線程,在申請鎖時會判斷是否有寫線程在同步隊列中等待。如果有,則讓步。不過這是為了防止寫線程餓死,與公平策略無關;如果是寫線程,則直接競爭鎖資源,不會關心有無別的線程正在等待
作者: 酒冽        出處: https://www.cnblogs.com/frankiedyz/p/15655865.html
版權:本文版權歸作者和博客園共有
轉載:歡迎轉載,但未經作者同意,必須保留此段聲明;必須在文章中給出原文連接;否則 必究法律責任

Sync類

Sync類是一個抽象類,有兩個具體子類NonfairSyncFairSync,分別對應非公平讀寫鎖公平讀寫鎖Sync類的主要作用就是為這兩個子類提供絕絕絕大部分的方法實現
只定義了兩個抽象方法writerShouldBlockreaderShouldBlocker交給兩個子類去實現

讀狀態和寫狀態

Sync類利用AQS單個state字段,來同時表示讀狀態寫狀態,源碼如下:

abstract static class Sync extends AbstractQueuedSynchronizer {
    private static final long serialVersionUID = 6317671515068378041L;

    /*
    * Read vs write count extraction constants and functions.
    * Lock state is logically divided into two unsigned shorts:
    * The lower one representing the exclusive (writer) lock hold count,
    * and the upper the shared (reader) hold count.
    */

    static final int SHARED_SHIFT   = 16;
    static final int SHARED_UNIT    = (1 << SHARED_SHIFT);
    static final int MAX_COUNT      = (1 << SHARED_SHIFT) - 1;
    static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1;
    
    /** Returns the number of shared holds represented in count  */
    static int sharedCount(int c)    { return c >>> SHARED_SHIFT; }
    /** Returns the number of exclusive holds represented in count  */
    static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; }
    
    // ······
    
};

根據上面源碼可以看出:

  • SHARED_SHIFT表示AQS中的state(int型,32位)的高16位,作為讀狀態,低16位作為寫狀態
  • SHARED_UNIT二級制為2^16,讀鎖加1,stateSHARED_UNIT
  • MAX_COUNT就是寫或讀資源的最大數量,為2^16-1
  • 使用sharedCount方法獲取讀狀態,使用exclusiveCount方法獲取獲取寫狀態

state划分為讀、寫狀態的示意圖(圖來自網絡)如下,其中讀鎖持有1個,寫鎖持有3個:

記錄首個獲得讀鎖的線程

private transient Thread firstReader = null;
private transient int firstReaderHoldCount;

firstReader記錄首個獲得讀鎖的線程firstReaderHoldCount記錄firstReader持有的讀鎖數

線程局部計數器

Sync類定義了一個線程局部變量readHolds,用於保存當前線程重入讀鎖的次數。如果該線程的讀鎖數減為0,則將該變量從線程局部域中移除。相關源碼如下:

// 內部類,用於記錄當前線程重入讀鎖的次數
static final class HoldCounter {
    int count = 0;
    // 這里使用線程的id而非直接引用,是為了方便GC
    final long tid = getThreadId(Thread.currentThread());
}

/* 內部類,繼承ThreadLocal,該類型的變量是每個線程各自保存一份,其中保存的是HoldCounter對象,用set方法保存,get方法獲取 */
static final class ThreadLocalHoldCounter
    extends ThreadLocal<HoldCounter> {
    public HoldCounter initialValue() {
        return new HoldCounter();
    }
}

private transient ThreadLocalHoldCounter readHolds;

由於readHolds變量是線程局部變量(繼承ThreadLocal類),每個線程都會保存一份副本,不同線程調用其get方法返回的HoldCounter對象不同

readHolds中的HoldCounter變量保存了每個讀線程的重入次數,即其持有的讀鎖數量。這么做的目的是便於線程釋放讀鎖時進行合法性判斷:線程在不持有讀鎖的情況下釋放鎖是不合法的,需要拋出IllegalMonitorStateException異常

緩存

Sync類定義了一個HoldCounter變量cachedHoldCounter,用於保存最近獲取到讀鎖的線程的重入次數。源碼如下:

// 這是一個啟發式算法
private transient HoldCounter cachedHoldCounter;

設計該變量的目的是:將其作為一個緩存,加快代碼執行速度。因為獲取、釋放讀鎖的線程往往都是最近獲取讀鎖的那個線程,雖然每個線程的重入次數都會使用readHolds來保存,但使用readHolds變量會涉及到ThreadLocal內部的查找(lookup),這是存在一定開銷的。有了cachedHoldCounter這個緩存后,就不用每次都在ThreadLocal內部查找,加快了代碼執行速度。相當於用空間換時間

獲取鎖

無論是公平鎖還是非公平鎖,它們獲取鎖的邏輯都是相同的,因此Sync類在這一層就提供了統一的實現

但是,獲取寫鎖和獲取讀鎖的邏輯不相同

  • 寫鎖是互斥資源,獲取寫鎖的邏輯主要在tryAcquire方法
  • 讀鎖是共享資源,獲取讀鎖的邏輯主要在tryAcquireShared方法

具體的源碼分析見下方的“讀鎖”和“寫鎖”各自章節的“獲取x鎖”部分

釋放鎖

無論是公平鎖還是非公平鎖,它們釋放鎖的邏輯都是相同的,因此Sync類在這一層就提供了統一的實現

但是,釋放寫鎖和釋放讀鎖的邏輯不相同

  • 寫鎖是互斥資源,釋放寫鎖的邏輯主要在tryRelease方法
  • 讀鎖是共享資源,釋放讀鎖的邏輯主要在tryReleaseShared方法

具體的源碼分析見下方的“讀鎖”和“寫鎖”各自章節的“釋放x鎖”部分

作者: 酒冽        出處: https://www.cnblogs.com/frankiedyz/p/15655865.html
版權:本文版權歸作者和博客園共有
轉載:歡迎轉載,但未經作者同意,必須保留此段聲明;必須在文章中給出原文連接;否則 必究法律責任

寫鎖

寫鎖是由內部類WriteLock實現的,其實現了Lock接口,獲取鎖、釋放鎖的邏輯都委托給了sync域(Sync對象)來執行。WriteLock的基本結構如下:

public static class WriteLock implements Lock, java.io.Serializable {
    private static final long serialVersionUID = -4992448646407690164L;
    private final Sync sync;

    // 構造方法注入Sync類對象
    protected WriteLock(ReentrantReadWriteLock lock) {
        sync = lock.sync;
    }
    
    // 實現了Lock接口的所有方法
}

獲取寫鎖

WriteLock使用lock方法獲取寫鎖,一次獲取一個寫鎖,源碼如下:

public void lock() {
    sync.acquire(1);
}

lock方法內部實際調用的是AQS的acquire方法,源碼如下:

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

acquire方法會調用子類Sync實現的tryAcquire方法,如下:

protected final boolean tryAcquire(int acquires) {
    Thread current = Thread.currentThread();
    int c = getState();
    int w = exclusiveCount(c);
    if (c != 0) {
        if (w == 0 || current != getExclusiveOwnerThread())
            return false;		// 如果是讀鎖被獲取中,或寫鎖被獲取但不是本線程獲取的,則獲取失敗
        if (w + exclusiveCount(acquires) > MAX_COUNT)
            throw new Error("Maximum lock count exceeded");
        // Reentrant acquire
        setState(c + acquires);
        return true;
    }
    if (writerShouldBlock() ||
        !compareAndSetState(c, c + acquires))
        return false;		// 如果根據公平性判斷此時寫線程需要被阻塞,或在獲取過程中發生競爭且競爭失敗,則獲取失敗
    setExclusiveOwnerThread(current);
    return true;
}

分為三步:
1、如果讀鎖正在被獲取中,或者寫鎖被獲取中但不是本線程持有,則獲取失敗
2、如果獲取寫鎖達到飽和,則拋出錯誤
3、如果上面兩個都不成立,說明此線程可以請求寫鎖。但需要先根據公平策略來判斷是否應該先阻塞。如果不用阻塞,且CAS成功,則獲取成功。否則獲取失敗

其中公平策略判斷所調用的writerShouldBlock,在后面分析公平鎖和非公平鎖時會給出分析

如果tryAcquire方法獲取寫鎖成功,則acquire方法直接返回,否則進入同步隊列阻塞等待

tryAcquire體現的讀寫鎖的特征

  • 互斥關系:
    • 寫鎖和寫鎖之間是互斥的:如果是別的線程持有寫鎖,那么直接返回false
    • 讀鎖和寫鎖之間是互斥的。當有線程持有讀鎖時,寫鎖不能獲得:如果c!=0w==0,說明此時有線程持有讀鎖,直接返回false
  • 可重入性:如果當前線程持有寫鎖,就不用進行公平性判斷writerShouldBlock,請求鎖一定會獲取成功
  • 不允許鎖升級:如果當前線程持有讀鎖,想要直接申請寫鎖,此時c!=0w==0,而exclusiveOwnerThread是null,不等於current,直接返回false

釋放寫鎖

WriteLock使用unlock方法釋放寫鎖,如下:

public void unlock() {
    sync.release(1);
}

unlock內部實際上調用的是AQS的release方法,源碼如下:

public final boolean release(int arg) {
    if (tryRelease(arg)) {
        Node h = head;
        if (h != null && h.waitStatus != 0)
            unparkSuccessor(h);
        return true;
    }
    return false;
}

而該方法會調用子類Sync實現的tryAcquire方法,源碼如下:

protected final boolean tryRelease(int releases) {
    // 如果並不持有鎖就釋放,會拋出異常
    if (!isHeldExclusively())
        throw new IllegalMonitorStateException();
    
    // 如果釋放鎖之后鎖空閑,那么需要將鎖持有者置為null
    int nextc = getState() - releases;
    boolean free = exclusiveCount(nextc) == 0;
    if (free)
        setExclusiveOwnerThread(null);
    setState(nextc);
    return free;		// 返回鎖釋放后是否空閑
}

注意:
任何鎖的釋放都需要判斷是否是在持有鎖的情況下。如果不持有鎖就釋放,會拋出異常。對於寫鎖來說,判斷是否持有鎖很簡單,只需要調用isHeldExclusively方法進行判斷即可;而對於讀鎖來說,判斷是否持有鎖比較復雜,需要根據每個線程各自保存的持有讀鎖數來判斷,即readHolds中保存的變量

嘗試獲取寫鎖

WriteLock使用tryLock來嘗試獲取寫鎖,如下:

public boolean tryLock( ) {
    return sync.tryWriteLock();
}

tryLock內部實際調用的是Sync類定義並實現的tryWriteLock方法。該方法是一個final方法,不允許子類重寫。其源碼如下:

final boolean tryWriteLock() {
    Thread current = Thread.currentThread();
    int c = getState();
    if (c != 0) {
        int w = exclusiveCount(c);
        if (w == 0 || current != getExclusiveOwnerThread())
            return false;
        if (w == MAX_COUNT)
            throw new Error("Maximum lock count exceeded");
    }
    if (!compareAndSetState(c, c + 1))		// 相比於tryAcquire方法,這里缺少對公平性判斷(writerShouldBlock)
        return false;
    setExclusiveOwnerThread(current);
    return true;
}

其實除了缺少對公平策略判斷writerShouldBlock的調用以外,和tryAcquire方法基本上是一樣的,這里不再廢話

Lock接口其他方法的實現

// 支持中斷響應的lock方法,實際上調用的是AQS的acquireInterruptibly方法
public void lockInterruptibly() throws InterruptedException {
    sync.acquireInterruptibly(1);
}

// 實際上調用的是AQS的方法tryAcquireNanos方法
public boolean tryLock(long timeout, TimeUnit unit)
    throws InterruptedException {
    return sync.tryAcquireNanos(1, unit.toNanos(timeout));
}

// 實際上調用的是Sync類實現的newCondition方法
public Condition newCondition() {
    return sync.newCondition();
}

寫鎖支持創建條件變量,因為寫鎖是獨占鎖,而條件變量在await時會釋放掉所有鎖資源。寫鎖能夠保證所有的鎖資源都是本線程所持有,所以可以放心地去釋放所有的鎖

而讀鎖不支持創建條件變量,因為讀鎖是共享鎖,可能會有其他線程持有讀鎖。如果調用await,不僅會釋放掉本線程持有的讀鎖,也會釋放掉其他線程持有的讀鎖,這是不被允許的。因此讀鎖不支持條件變量

作者: 酒冽        出處: https://www.cnblogs.com/frankiedyz/p/15655865.html
版權:本文版權歸作者和博客園共有
轉載:歡迎轉載,但未經作者同意,必須保留此段聲明;必須在文章中給出原文連接;否則 必究法律責任

讀鎖

讀鎖是由內部類ReadLock實現的,其實現了Lock接口,獲取鎖、釋放鎖的邏輯都委托給了Sync類實例sync來執行。ReadLock的基本結構如下:

public static class ReadLock implements Lock, java.io.Serializable {
    private static final long serialVersionUID = -5992448646407690164L;
    private final Sync sync;

    // 構造方法注入Sync類對象
    protected ReadLock(ReentrantReadWriteLock lock) {
        sync = lock.sync;
    }
	
    // 實現了Lock接口的所有方法
}

獲取讀鎖

ReadLock使用lock方法獲取讀鎖,一次獲取一個讀鎖。源碼如下:

public void lock() {
    sync.acquireShared(1);
}

lock方法內部實際調用的是AQS的acquireShared方法,源碼如下:

public final void acquireShared(int arg) {
    if (tryAcquireShared(arg) < 0)
        doAcquireShared(arg);
}

該方法會調用Sync類實現的tryAcquireShared方法,源碼如下:

protected final int tryAcquireShared(int unused) {
    Thread current = Thread.currentThread();
    int c = getState();
    if (exclusiveCount(c) != 0 &&
        getExclusiveOwnerThread() != current)
        return -1;			// 如果寫鎖被獲取,且並不是由本線程持有寫鎖,那么獲取失敗
    int r = sharedCount(c);
    if (!readerShouldBlock() &&		// 先進行公平性判斷是否應該讓步,這可能會導致重入讀鎖的線程獲取失敗
        r < MAX_COUNT &&
        compareAndSetState(c, c + SHARED_UNIT)) {		// CAS失敗可能也會導致本能獲取成功的線程獲取失敗
        // 如果此時讀鎖沒有被獲取,則該線程是第一個獲取讀鎖的線程,記錄相應信息
        if (r == 0) {
            firstReader = current;
            firstReaderHoldCount = 1;
        } else if (firstReader == current) {
            firstReaderHoldCount++;
        }
        // 該線程不是首個獲取讀鎖的線程,需要記錄到readHolds中
        else {
            HoldCounter rh = cachedHoldCounter;		// 通常當前獲取讀鎖的線程就是最近獲取到讀鎖的線程,所以直接用緩存
            // 還是需要判斷一下是不是最近獲取到讀鎖的線程。如果不是,則調用get創建一個新的局部HoldCounter變量
            if (rh == null || rh.tid != getThreadId(current))
                cachedHoldCounter = rh = readHolds.get();
            // 之前最近獲取讀鎖的線程如果釋放完了讀鎖而導致其局部HoldCounter變量被remove了,這里重新獲取就重新set
            else if (rh.count == 0)
                readHolds.set(rh);
            rh.count++;
        }
        return 1;	// 如果公平性判斷無需讓步,且讀鎖數未飽和,且CAS競爭成功,則說明獲取成功
    }
    return fullTryAcquireShared(current);
}

tryAcquireShared的返回值說明:

  • 負數:獲取失敗,線程會進入同步隊列阻塞等待
  • 0:獲取成功,但是后續以共享模式獲取的線程都不可能獲取成功(這里暫時用不上)
  • 正數:獲取成功,且后續以共享模式獲取的線程也可能獲取成功

在讀寫鎖中,tryAcquireShared沒有返回0的情況,只會返回正數或負數

前面“Sync”中講解過這些變量,這里再復習一遍:

  • firstReaderfirstReaderHoldCount分別用於記錄第一個獲取到寫鎖的線程及其持有讀鎖的數量
  • cachedHoldCounter用於記錄最近獲取到寫鎖的線程持有讀鎖的數量
  • readHolds是一個線程局部變量ThreadLocal變量),用於保存每個獲得讀鎖的線程各自持有的讀鎖數量

tryAcquireShared的流程如下:
1、如果其他線程持有寫鎖,那么獲取失敗(返回-1)
2、否則,根據公平策略判斷是否應該阻塞。如果不用阻塞且讀鎖數量未飽和,則CAS請求讀鎖。如果CAS成功,獲取成功(返回1),並記錄相關信息
3、如果根據公平策略判斷應該阻塞,或者讀鎖數量飽和,或者CAS競爭失敗,那么交給完整版本的獲取方法fullTryAcquireShared去處理

其中上述步驟2如果發生了重入讀(當前線程持有讀鎖的情況下,再次請求讀鎖),但根據公平策略判斷該線程需要阻塞等待,而導致重入讀失敗。按照正常邏輯,重入讀不應該失敗。不過,tryAcquireShared並沒有處理這種情況,而是將其放到了fullTryAcquireShared中進行處理。此外,CAS競爭失敗而導致獲取讀鎖失敗,也交給fullTryAcquireShared去處理(fullTryAcquireShared表示我好難-_-)

fullTryAcquireShared方法是嘗試獲取讀鎖的完全版本,用於處理tryAcquireShared方法未處理的:
1、CAS競爭失敗
2、因公平策略判斷應該阻塞而導致的重入讀失敗

這兩種情況。其源碼如下:

final int fullTryAcquireShared(Thread current) {
    HoldCounter rh = null;
    for (;;) {
        int c = getState();
        if (exclusiveCount(c) != 0) {
            if (getExclusiveOwnerThread() != current)
                return -1;
            // else we hold the exclusive lock; blocking here
            // would cause deadlock.
        } else if (readerShouldBlock()) {
            // 如果當前線程就是firstReader,那么它一定是重入讀,不讓它失敗,而是重新loop直到公平性判斷不阻塞為止
            if (firstReader == current) {
                // assert firstReaderHoldCount > 0;
            } else {
                if (rh == null) {
                    rh = cachedHoldCounter;
                    if (rh == null || rh.tid != getThreadId(current)) {
                        rh = readHolds.get();
                        if (rh.count == 0)
                            readHolds.remove();
                    }
                }
                if (rh.count == 0)		// 當前線程既不持有讀鎖(不是重入讀),並且被公平性判斷為應該阻塞,那么就獲取失敗
                    return -1;
            }
        }
        if (sharedCount(c) == MAX_COUNT)
            throw new Error("Maximum lock count exceeded");
        
        // 下面的邏輯基本上和tryAcquire中差不多,不過這里的CAS如果失敗,會重新loop直到成功為止
        if (compareAndSetState(c, c + SHARED_UNIT)) {
            if (sharedCount(c) == 0) {
                firstReader = current;
                firstReaderHoldCount = 1;
            } else if (firstReader == current) {
                firstReaderHoldCount++;
            } else {
                if (rh == null)
                    rh = cachedHoldCounter;
                if (rh == null || rh.tid != getThreadId(current))
                    rh = readHolds.get();
                else if (rh.count == 0)
                    readHolds.set(rh);
                rh.count++;
                cachedHoldCounter = rh; // cache for release
            }
            return 1;
        }
    }
}

fullTryAcquireShared其實和tryAcquire存在很多的冗余之處,但這么做的目的主要是讓tryAcquireShared變得更簡單,不用處理復雜的CAS循環

fullTryAcquireShared主要是為了處理CAS失敗readerShouldBlock判true而導致的重入讀失敗,這兩種情況在理論上都應該成功獲取鎖fullTryAcquireShared的做法就是將這兩種情況放在for循環中,一旦發生就重新循環,直到成功為止

tryAcquireSharedfullTryAcquireShared體現的讀寫鎖特征

  • 互斥關系:
    • 讀鎖和讀鎖之間是共享的:即使有其他線程持有了讀鎖,當前線程也能獲取讀鎖
    • 讀鎖和寫鎖之間是互斥的。當有其他線程持有寫鎖,讀鎖不能獲得:tryAcquireShared第4-6行fullTryAcquireShared第5-7行都能體現這一特征
  • 可重入性:如果當前線程獲取了讀鎖,那么它再次申請讀鎖一定能成功。這部分邏輯是由fullTryAcquireSharedfor循環實現的
  • 支持鎖降級:如果當前線程持有寫鎖,那么它申請讀鎖一定會成功。這部分邏輯見tryAcquireShared第5行currentexclusiveOwnerThread是相等的,不會返回-1

釋放讀鎖

ReadLock使用unlock方法釋放讀鎖,如下:

public void unlock() {
    sync.releaseShared(1);
}

unlock方法實際調用的是AQS的releaseShared方法,如下:

public final boolean releaseShared(int arg) {
    if (tryReleaseShared(arg)) {
        doReleaseShared();
        return true;
    }
    return false;
}

而該方法會調用Sync類實現的tryReleaseShared方法,源碼如下:

protected final boolean tryReleaseShared(int unused) {
    Thread current = Thread.currentThread();
    if (firstReader == current) {
        if (firstReaderHoldCount == 1)
            firstReader = null;
        else
            firstReaderHoldCount--;
    } else {
        HoldCounter rh = cachedHoldCounter;		// 一般釋放鎖的都是最后獲取鎖的那個線程
        if (rh == null || rh.tid != getThreadId(current))
            rh = readHolds.get();
        int count = rh.count;
        if (count <= 1) {
            readHolds.remove();		// 如果釋放讀鎖后不再持有鎖,那么移除readHolds保存的線程局部HoldCounter變量
            if (count <= 0)
                throw unmatchedUnlockException();	// 拋出IllegalMonitorStateException異常
        }
        --rh.count;
    }
    // 循環CAS保證修改state成功
    for (;;) {
        int c = getState();
        int nextc = c - SHARED_UNIT;
        if (compareAndSetState(c, nextc))
            return nextc == 0;		// 如果釋放后鎖空閑,那么返回true,否則返回false
    }
}

如果返回true,說明鎖是空閑的,releaseShared方法會進一步調用doReleaseShared方法,doReleaseShared方法會喚醒后繼線程並確保傳播(確保傳播:保證被喚醒的線程可以執行喚醒其后續線程的邏輯

嘗試釋放讀鎖

ReadLock使用tryLock方法嘗試釋放讀鎖,源碼如下:

public boolean tryLock() {
    return sync.tryReadLock();
}

tryLock內部實際調用的是Sync類定義並實現的tryReadLock方法。該方法是一個final方法,不允許子類重寫。其源碼如下:

final boolean tryReadLock() {
    Thread current = Thread.currentThread();
    for (;;) {
        int c = getState();
        if (exclusiveCount(c) != 0 &&
            getExclusiveOwnerThread() != current)
            return false;
        int r = sharedCount(c);
        if (r == MAX_COUNT)
            throw new Error("Maximum lock count exceeded");
        if (compareAndSetState(c, c + SHARED_UNIT)) {
            if (r == 0) {
                firstReader = current;
                firstReaderHoldCount = 1;
            } else if (firstReader == current) {
                firstReaderHoldCount++;
            } else {
                HoldCounter rh = cachedHoldCounter;
                if (rh == null || rh.tid != getThreadId(current))
                    cachedHoldCounter = rh = readHolds.get();
                else if (rh.count == 0)
                    readHolds.set(rh);
                rh.count++;
            }
            return true;
        }
    }
}

其實除了缺少對公平策略判斷方法readerShouldBlock的調用以外,和tryAcquireShared方法基本上是一樣的

Lock接口其他方法的實現

// 支持中斷響應的lock方法,實際上調用的是AQS的acquireSharedInterruptibly方法
public void lockInterruptibly() throws InterruptedException {
    sync.acquireSharedInterruptibly(1);
}

// 實際上調用的是AQS的方法tryAcquireSharedNanos方法
public boolean tryLock(long timeout, TimeUnit unit)
    throws InterruptedException {
    return sync.tryAcquireSharedNanos(1, unit.toNanos(timeout));
}

// 讀鎖不支持創建條件變量
public Condition newCondition() {
    throw new UnsupportedOperationException();
}

和寫鎖的區別在於,讀鎖不支持創建條件變量。如果調用newCondition方法,會直接拋出UnsupportedOperationException異常。不支持的原因在前面已經分析過,這里不再贅述

作者: 酒冽        出處: https://www.cnblogs.com/frankiedyz/p/15655865.html
版權:本文版權歸作者和博客園共有
轉載:歡迎轉載,但未經作者同意,必須保留此段聲明;必須在文章中給出原文連接;否則 必究法律責任

讀寫鎖的公平策略

ReentrantReadWriteLock默認構造方法如下:

public ReentrantReadWriteLock() {
    this(false);
}

public ReentrantReadWriteLock(boolean fair) {
    sync = fair ? new FairSync() : new NonfairSync();
    readerLock = new ReadLock(this);
    writerLock = new WriteLock(this);
}

說明其默認創建的是非公平讀寫鎖。如果要創建公平讀寫鎖,需要使用有參構造函數,參數fair設置為true

公平讀寫鎖

公平讀寫鎖依賴於Sync的子類FairSync來實現,其源碼如下:

static final class FairSync extends Sync {
    private static final long serialVersionUID = -2274990926593161451L;
    final boolean writerShouldBlock() {
        return hasQueuedPredecessors();
    }
    final boolean readerShouldBlock() {
        return hasQueuedPredecessors();
    }
}

writerShouldBlock

writerShouldBlock實際上調用的是AQS的hasQueuedPredecessors方法,該方法會檢查是否有線程在同步隊列中等待,源碼如下:

public final boolean hasQueuedPredecessors() {
    Node t = tail;
    Node h = head;
    Node s;
    // 如果head等於tail,說明是空隊列
    // 如果隊首的thread域不是當前線程,說明有別的線程先於當前線程等待獲取鎖
    return h != t &&
        ((s = h.next) == null || s.thread != Thread.currentThread());
}

writerShouldBlock只有在tryAcquire中被調用。如果當前線程請求寫鎖時發現已經有線程(讀線程or寫線程)在同步隊列中等待,則讓步

readerShouldBlock

readerShouldBlockwriterShouldBlock一樣,都是調用AQS的hasQueuedPredecessors方法

readerShouldBlock只有在tryAcquireSharedfullTryAcquireShared)中被調用。如果當前線程請求讀鎖時發現已經有線程(讀線程or寫線程)在同步隊列中等待,則讓步

非公平讀寫鎖

非公平讀寫鎖依賴於Sync的子類NonfairSync來實現,其源碼如下:

static final class NonfairSync extends Sync {
    private static final long serialVersionUID = -8159625535654395037L;
    final boolean writerShouldBlock() {
        return false; // writers can always barge
    }
    final boolean readerShouldBlock() {
        return apparentlyFirstQueuedIsExclusive();
    }
}

writerShouldBlock

writerShouldBlock直接返回false

writerShouldBlock只有在tryAcquire中被調用,返回false表示在非公平模式下,不管是否有線程在同步隊列中等待,請求寫鎖都不會讓步,而是直接上去競爭

readerShouldBlock

readerShouldBlock實際調用的是AQS的apparentlyFirstQueuedIsExclusive方法。其源碼如下:

final boolean apparentlyFirstQueuedIsExclusive() {
    Node h, s;
    return (h = head) != null &&
        (s = h.next)  != null &&
        !s.isShared()         &&
        s.thread != null;
}

如果同步隊列為空,或隊首線程是讀線程(獲取讀鎖而被阻塞),則返回false。如果同步隊列隊首線程是寫線程(獲取寫鎖而被阻塞),則返回true

readerShouldBlock只有在tryAcquireSharedfullTryAcquireShared)中被調用。如果當前線程請求讀鎖時發現同步隊列隊首線程是寫線程,則讓步。如果是讀線程則跟它爭奪鎖資源

這么做的目的是為了防止寫線程被“餓死”。因為如果一直有讀線程前來請求鎖,且讀鎖是有求必應,就會使得在同步隊列中的寫線程一直不能被喚醒。不過,apparentlyFirstQueuedIsExclusive只是一種啟發式算法,並不能保證寫線程一定不會被餓死。因為寫線程有可能不在同步隊列隊首,而是排在其他讀線程后面

讀寫鎖的公平策略總結

公平模式:
無論當前線程請求寫鎖還是讀鎖,只要發現此時還有別的線程在同步隊列中等待(寫鎖or讀鎖),都一律選擇讓步

非公平模式:

  • 請求寫鎖時,當前線程會選擇直接競爭,不會做絲毫的讓步
  • 請求讀鎖時,如果發現同步隊列隊首線程在等待獲取寫鎖,則會讓步。不過這是一種啟發式算法,因為寫線程可能排在其他讀線程后面

如果覺得作者寫的還可以的話,可以👍鼓勵一下


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM