ReentrantReadWriteLock 底層原理

1.讀寫鎖自定義

當讀操作遠遠高於寫操作時,這時候使用 讀寫鎖讀-讀 可以併發,提高性能。 類似於數據庫中的 select ... from ... lock in share mode提供一個 數據容器類 內部分別使用讀鎖保護數據的 read() 方法,寫鎖保護數據的 write()方法

自定義數據容器類

class DataContainer {
private Object data;
private ReentrantReadWriteLock rw = new ReentrantReadWriteLock();
private ReentrantReadWriteLock.ReadLock r = rw.readLock();
private ReentrantReadWriteLock.WriteLock w = rw.writeLock();
public Object read() {
log.debug("獲取讀鎖...");
r.lock();
try {
log.debug("讀取");
sleep(1);
return data;
} finally {
log.debug("釋放讀鎖...");
r.unlock();
}
}
public void write() {
log.debug("獲取寫鎖...");
w.lock();
try {
log.debug("寫入");
sleep(1);
} finally {
log.debug("釋放寫鎖...");
w.unlock();
}
}
}

測試 讀鎖-讀鎖 可以併發,讀鎖沒有釋放時,其他線程就可以獲取讀鎖

DataContainer dataContainer = new DataContainer();
new Thread(() -> {
dataContainer.read();
}, "t1").start();
new Thread(() -> {
dataContainer.read();
}, "t2").start()

結果:輸出結果,從這裏可以看到 Thread-0 鎖定期間,
Thread-1 的讀操作不受影響
14:05:14.341 c.DataContainer [t2] - 獲取讀鎖...
14:05:14.341 c.DataContainer [t1] - 獲取讀鎖...
14:05:14.345 c.DataContainer [t1] - 讀取
14:05:14.345 c.DataContainer [t2] - 讀取
14:05:15.365 c.DataContainer [t2] - 釋放讀鎖...
14:05:15.386 c.DataContainer [t1] - 釋放讀鎖...

測試 讀鎖-寫鎖 相互阻塞,一方線程要等另一方線程鎖釋放

DataContainer dataContainer = new DataContainer();
new Thread(() -> {
dataContainer.read();
}, "t1").start();
Thread.sleep(100);
new Thread(() -> {
dataContainer.write();
}, "t2").start();

結果:
14:04:21.838 c.DataContainer [t1] - 獲取讀鎖...
14:04:21.838 c.DataContainer [t2] - 獲取寫鎖...
14:04:21.841 c.DataContainer [t2] - 寫入
14:04:22.843 c.DataContainer [t2] - 釋放寫鎖...
14:04:22.843 c.DataContainer [t1] - 讀取
14:04:23.843 c.DataContainer [t1] - 釋放讀鎖...

寫鎖-寫鎖 也是相互阻塞的,必須要等一方線程釋放鎖,下一個線程才能操作,這裏不作測試了


注意事項

  • 讀鎖不支持條件變量
  • 重入時升級不支持:即持有讀鎖的情況下去獲取寫鎖,會導致獲取寫鎖永久等待
r.lock();//讀鎖
try {
// ...
w.lock();//寫鎖永久等待
try {
// ...
} finally{
w.unlock();
}
} finally{
r.unlock();
}
  • 重入時降級支持:即持有寫鎖的情況下去獲取讀鎖(這是同一個線程重入鎖可以)
class CachedData {
Object data;
// 是否有效,如果失效,需要重新計算 data
volatile boolean cacheValid;
final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
void processCachedData() {
rwl.readLock().lock();
if (!cacheValid) {
// 獲取寫鎖前必須釋放讀鎖
rwl.readLock().unlock();
rwl.writeLock().lock();
try {
// 判斷是否有其它線程已經獲取了寫鎖、更新了緩存, 避免重複更新
if (!cacheValid) {
data = ...
cacheValid = true;
}
// 降級爲讀鎖, 釋放寫鎖, 這樣能夠讓其它線程讀取緩存
rwl.readLock().lock();
} finally {
rwl.writeLock().unlock();
}
}
// 自己用完數據, 釋放讀鎖
try {
use(data);
} finally {
rwl.readLock().unlock();
}
}
}

2. ReentrantReadWriteLock底層原理

讀寫鎖用的是同一個 Sycn 同步器,因此等待隊列、state 等也是同一個

2.1. t1 w.lock,t2 r.lock

ReentrantReadWriteLock類結構:在這裏插入圖片描述

2.1.1. 寫鎖上鎖流程(跟ReentrantLock一樣,t1 w.lock是獨佔鎖/排它鎖)

1). acquire(int arg)方法

static final class NonfairSync extends Sync {
// ... 省略無關代碼
// 外部類 WriteLock 方法, 方便閱讀, 放在此處
public void lock() {
sync.acquire(1);
}
// AQS 繼承過來的方法, 方便閱讀, 放在此處
public final void acquire(int arg) {
if (
// 嘗試獲得寫鎖失敗
!tryAcquire(arg) &&
// 將當前線程關聯到一個 Node 對象上, 模式爲獨佔模式
// 進入 AQS 隊列阻塞
acquireQueued(addWaiter(Node.EXCLUSIVE), arg)
) {
selfInterrupt();
}
}
}

2). tryAcquire(arg)方法(子類的實現方法)

t1 成功上鎖,流程與 ReentrantLock 加鎖相比沒有特殊之處,
不同是寫鎖狀態佔了 state 的低 16 位,而讀鎖使用的
是 state 的高 16 位

在這裏插入圖片描述

// Sync 繼承過來的方法, 方便閱讀, 放在此處
protected final boolean tryAcquire(int acquires) {
// 獲得低 16 位, 代表寫鎖的 state 計數
Thread current = Thread.currentThread();
int c = getState();//獲取寫鎖當前的同步狀態
//exclusiveCount(c)獲取寫鎖獲取的次數
int w = exclusiveCount(c);
if (c != 0) {
if (
// c != 0 and w == 0 表示有讀鎖, 或者
w == 0 ||
// 如果 exclusiveOwnerThread 不是自己
current != getExclusiveOwnerThread()
) {
// 獲得鎖失敗
return false;
}
// 寫鎖計數超過低 16 位, 報異常
if (w + exclusiveCount(acquires) > MAX_COUNT)
throw new Error("Maximum lock count exceeded");
// 寫鎖重入, 獲得鎖成功
setState(c + acquires);
return true;
}
if (
// 判斷寫鎖是否該阻塞, 或者
writerShouldBlock() ||
// 嘗試更改計數失敗
!compareAndSetState(c, c + acquires)
) {
// 獲得鎖失敗
return false;
}
// 獲得鎖成功
setExclusiveOwnerThread(current);
return true;
}

在這裏插入圖片描述
3)exclusiveCount( c)方法寫鎖被獲取的次數

其中EXCLUSIVE_MASK爲: static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1; EXCLUSIVE _MASK爲1左移16位然後減1,即爲0x0000FFFF。而exclusiveCount方法是將同步狀態(state爲int類型)與0x0000FFFF相與,即取同步狀態的低16位。那麼低16位代表什麼呢?根據exclusiveCount方法的註釋爲獨佔式獲取的次數即寫鎖被獲取的次數,現在就可以得出來一個結論同步狀態的低16位用來表示寫鎖的獲取次數

static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; }

4)writerShouldBlock()方法判斷寫鎖是否該阻塞
該方法是Sync類中的抽象方法,有公平鎖和非公平鎖兩種實現方式:在這裏插入圖片描述
對於非公平鎖:

static final class NonfairSync extends Sync {
    //對於非公平鎖總是返回false,不需要阻塞
    final boolean writerShouldBlock() {
        return false; 
    }
}

對於公平鎖:

static final class FairSync extends Sync {
    //對於公平鎖,需要判斷
    final boolean writerShouldBlock() {
        return hasQueuedPredecessors();
    }
}

5)如果嘗試獲取鎖tryAcquire失敗,就進入
acquireQueued(addWaiter(Node.EXCLUSIVE), arg)方法,與ReentrantLock一樣,這裏不作細述


2.1.2. 讀鎖上鎖流程(t2 r.lock共享鎖)

1) acquireShared(arg)方法

static final class NonfairSync extends Sync {
// ReadLock 方法, 方便閱讀, 放在此處
public void lock() {
sync.acquireShared(1);
}
// AQS 繼承過來的方法, 方便閱讀, 放在此處
public final void acquireShared(int arg) {
// tryAcquireShared 返回負數, 表示獲取讀鎖失敗
if (tryAcquireShared(arg) < 0) {
doAcquireShared(arg);
}
}
}

2) tryAcquireShared(arg)方法

t2 執行 r.lock,這時進入讀鎖的 sync.acquireShared(1) 
流程,首先會進入 tryAcquireShared 流程。如果有寫鎖佔據
並且獲取寫鎖的線程不是當前線程,那麼 tryAcquireShared 
返回 -1 表示失敗。

tryAcquireShared 返回值表示
* -1 表示失敗
* 0 表示成功,但後繼節點不會繼續喚醒
* 正數表示成功,而且數值是還有幾個後繼節點需要喚醒,讀寫鎖返回 1

在這裏插入圖片描述

// Sync 繼承過來的方法, 方便閱讀, 放在此處
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)
) {
// ... 省略不重要的代碼
return 1;
}
return fullTryAcquireShared(current);
}

3) sharedCount( c) 讀鎖被獲取的次數
在這裏插入圖片描述
該方法是獲取讀鎖被獲取的次數,是將同步狀態(int c)右移16次,即取同步狀態的高16位,現在我們可以得出另外一個結論同步狀態的高16位用來表示讀鎖被獲取的次數。讀寫鎖是怎樣實現分別記錄讀鎖和寫鎖的狀態的,就是通過獲取讀寫鎖的次數,和exclusiveCount(int c)獲取寫鎖次數一樣

static int sharedCount(int c)    
{ return c >>> SHARED_SHIFT; }

4)readerShouldBlock()方法 判斷讀鎖是否該阻塞
這個方法對於公平鎖和非公平鎖的實現是不同的,也就導致了ReentrantReadWriteLock()對於公平和非公平的兩種不同實現:

對於非公平鎖:

static final class NonfairSync extends Sync {
	//...
    final boolean readerShouldBlock() {
/**
	看 AQS 隊列中第一個節點是否是寫鎖,true 則該阻塞, false 則不阻塞:
  	由於非公平的競爭,並且讀鎖可以共享,所以可能會出現源源不斷的讀,使得寫鎖永遠競爭不到,然後出現餓死的現象(讀-讀可以共享,讀-寫阻塞需要等待釋放)
    通過這個策略,當一個寫鎖出現在頭結點後面的時候,會立刻阻塞所有還未獲取讀鎖的其他線程,讓步給寫線程先執行(寫-讀阻塞)
*/
        return apparentlyFirstQueuedIsExclusive();
    }
}

公平鎖:

static final class FairSync extends Sync {
	//...
    final boolean readerShouldBlock() {
    	//對於公平鎖來說,如果有前驅(也就是非頭結點),都會進行等待,不允許競爭鎖
        return hasQueuedPredecessors();
    }
}

5) fullTryAcquireShared(current)

// AQS 繼承過來的方法, 方便閱讀, 放在此處
// 與 tryAcquireShared 功能類似, 但會不斷嘗試 for (;;) 獲取讀鎖, 執行過程中無阻塞
final int fullTryAcquireShared(Thread current) {
HoldCounter rh = null;
for (;;) {
int c = getState();
if (exclusiveCount(c) != 0) {
if (getExclusiveOwnerThread() != current)
return -1;
} else if (readerShouldBlock()) {
// ... 省略不重要的代碼
}
if (sharedCount(c) == MAX_COUNT)
throw new Error("Maximum lock count exceeded");
if (compareAndSetState(c, c + SHARED_UNIT)) {
// ... 省略不重要的代碼
return 1;
}
}
}

如果獲取讀鎖獲取失敗,就會繼續執行下面的doAcquireShared(arg)方法:想象成acquireQueued()方法

6) doAcquireShared(arg)方法

1)如果t2線程獲取鎖失敗,這時會進入doAcquireShared(1) 流程,
首先也是調用 addWaiter 添加節點,不同之處在於節點被設置爲
Node.SHARED 模式而非 Node.EXCLUSIVE 模式,
注意此時 t2 仍處於活躍狀態 。

在這裏插入圖片描述

2)t2 會看看自己的節點是不是老二,如果是,還會再次調用 
tryAcquireShared(1) 來嘗試獲取鎖
3)如果沒有成功,在 doAcquireShared 內 for (;;) 循環一次,
把前驅節點的 waitStatus 改爲 -1,再 for (;;) 循環一
次嘗試 tryAcquireShared(1) 如果還不成功,
那麼在 parkAndCheckInterrupt() 處 park

在這裏插入圖片描述

// AQS 繼承過來的方法, 方便閱讀, 放在此處
private void doAcquireShared(int arg) {
// 將當前線程關聯到一個 Node 對象上, 模式爲共享模式
final Node node = addWaiter(Node.SHARED);
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor();
if (p == head) {
// 再一次嘗試獲取讀鎖
int r = tryAcquireShared(arg);
// 成功
if (r >= 0) {
// ㈠
// r 表示可用資源數, 在這裏總是 1 允許傳播
//(喚醒 AQS 中下一個 Share 節點)
setHeadAndPropagate(node, r);
p.next = null; // help GC
if (interrupted)
selfInterrupt();
failed = false;
return;
}
}
if (
// 是否在獲取讀鎖失敗時阻塞(前一個階段 waitStatus == Node.SIGNAL)
shouldParkAfterFailedAcquire(p, node) &&
// park 當前線程
parkAndCheckInterrupt()
) {
interrupted = true;
}
}
} finally {
if (failed)
cancelAcquire(node);
}
}

2.2 t3 r.lock,t4 w.lock

這種狀態下,假設又有 t3 加讀鎖和 t4 加寫鎖,這
期間 t1 仍然持有鎖,就變成了下面的樣子

在這裏插入圖片描述

2.3 寫鎖釋放(t1 w.unlock)

2.3.1 寫鎖釋放流程及讀鎖加鎖流程

1、release()方法

static final class NonfairSync extends Sync {
// ... 省略無關代碼
// WriteLock 方法, 方便閱讀, 放在此處
public void unlock() {
sync.release(1);
}
// AQS 繼承過來的方法, 方便閱讀, 放在此處
public final boolean release(int arg) {
// 嘗試釋放寫鎖成功
if (tryRelease(arg)) {
// unpark AQS 中等待的線程
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}

2、tryRelease()方法
在這裏插入圖片描述

// Sync 繼承過來的方法, 方便閱讀, 放在此處
protected final boolean tryRelease(int releases) {
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
int nextc = getState() - releases;
// 因爲可重入的原因, 寫鎖計數爲 0, 纔算釋放成功
boolean free = exclusiveCount(nextc) == 0;
if (free) {
setExclusiveOwnerThread(null);
}
setState(nextc);
return free;
}
}
這時會走到寫鎖的 sync.release(1) 流程,調用 
sync.tryRelease(1) 成功,變成下面的樣子 :

在這裏插入圖片描述
3、unparkSuccessor ()方法

private void unparkSuccessor(Node node) {
    int ws = node.waitStatus;
    if (ws < 0)
        //將當前線程的節點狀態置0
        compareAndSetWaitStatus(node, ws, 0);
    
	//找到下一個需要喚醒的結點s
    Node s = node.next;
    if (s == null || s.waitStatus > 0) {
        s = null;
        //如果該節點已經取消獲取鎖,那就從隊尾開始向前找,找到第一個ws<=0的節點,並賦值給s
        for (Node t = tail; t != null && t != node; t = t.prev)
            if (t.waitStatus <= 0)
                s = t;
    }
    //調用unpark()方法,喚醒正在阻塞的線程
    if (s != null)
        LockSupport.unpark(s.thread);
}
接下來執行喚醒流程sync.unparkSuccessor,即讓老二恢復運行:

在這裏插入圖片描述
4、doAcquireShared()方法
這裏開始喚醒讀鎖,加鎖了

這時 t2 在doAcquireShared 內parkAndCheckInterrupt()處恢復運行,
這回再來一次 for (;;) 執行 tryAcquireShared 成功則讓讀鎖計數加一

在這裏插入圖片描述

private void doAcquireShared(int arg) {
    final Node node = addWaiter(Node.SHARED);
    boolean failed = true;
    try {
        boolean interrupted = false;
        for (;;) {
            final Node p = node.predecessor();
            if (p == head) {
                //2、繼續嘗試獲取鎖資源,讓讀鎖計數加1
                int r = tryAcquireShared(arg);
                if (r >= 0) {
                    //3、喚醒下一個線程
                    setHeadAndPropagate(node, r);
                    p.next = null; // help GC
                    if (interrupted)
                        selfInterrupt();
                    failed = false;
                    return;
                }
            }
           	//1、t2線程在這兒被喚醒,就會繼續指向一次for循環
            if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

5、setHeadAndPropagate (node, 1)方法

這時 t2 已經恢復運行,接下來 t2 調用 setHeadAndPropagate(node, 1),
它原本所在節點被置爲頭節點

在這裏插入圖片描述

private void setHeadAndPropagate(Node node, int propagate) {
    Node h = head;
    setHead(node);//head指向自己
     //如果鎖計數>0,就繼續喚醒下面的線程
    if (propagate > 0 || h == null || h.waitStatus < 0) {
        Node s = node.next;
        //檢查下一個節點是否是 shared,如果是將 head 的狀態從 -1 改爲 0 並喚醒老二
        if (s == null || s.isShared())
            doReleaseShared();
    }
}
事情還沒完,在setHeadAndPropagate方法內還會檢查下一個節點是否是 
shared,如果是則調用doReleaseShared() 將 head 的狀態從 -1 
改爲 0 並喚醒老二,這時 t3 在 doAcquireShared內
parkAndCheckInterrupt() 處恢復運行

在這裏插入圖片描述

這回再來一次 for (;;) 執行 tryAcquireShared 成功則讓讀鎖計數加一

在這裏插入圖片描述

這時 t3 已經恢復運行,接下來 t3 調用 setHeadAndPropagate(node, 1),它原本所在節點被置爲頭節點

在這裏插入圖片描述

下一個節點不是 shared 了,是寫鎖(和ReentrantLock一樣,獨佔鎖)因此不會繼續喚醒 t4 所在節點

2.4 t2 r.unlock,t3 r.unlock

2.4.1 讀鎖釋放流程與寫鎖加鎖流程

1、releaseShared(int arg)方法

static final class NonfairSync extends Sync {
// ReadLock 方法, 方便閱讀, 放在此處
public void unlock() {
sync.releaseShared(1);
}
// AQS 繼承過來的方法, 方便閱讀, 放在此處
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
doReleaseShared();
return true;
}
return false;
}

2、tryReleaseShared(int unused)方法

t2 進入 sync.releaseShared(1) 中,調用 tryReleaseShared(1) 
讓計數減一,但由於計數還不爲零

在這裏插入圖片描述

// Sync 繼承過來的方法, 方便閱讀, 放在此處
protected final boolean tryReleaseShared(int unused) {
// ... 省略不重要的代碼
for (;;) {
int c = getState();
int nextc = c - SHARED_UNIT;
if (compareAndSetState(c, nextc)) {
// 讀鎖的計數不會影響其它獲取讀鎖線程, 但會影響其它獲取寫鎖線程
// 計數爲 0 纔是真正釋放
return nextc == 0;
}
}
}

3、doReleaseShared()方法

t3 進入 sync.releaseShared(1) 中,調用 tryReleaseShared(1) 
讓計數減一,這回計數爲零了,進入doReleaseShared() 
將頭節點從 -1 改爲 0 並喚醒老二,即

在這裏插入圖片描述

// AQS 繼承過來的方法, 方便閱讀, 放在此處
private void doReleaseShared() {
// 如果 head.waitStatus == Node.SIGNAL ==> 0 成功, 下一個節點 unpark
// 如果 head.waitStatus == 0 ==> Node.PROPAGATE
for (;;) {
Node h = head;
if (h != null && h != tail) {
int ws = h.waitStatus;
// 如果有其它線程也在釋放讀鎖,那麼需要將 waitStatus 先改爲 0
// 防止 unparkSuccessor 被多次執行
if (ws == Node.SIGNAL) {
if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
continue; // loop to recheck cases
unparkSuccessor(h);
}
// 如果已經是 0 了,改爲 -3,用來解決傳播性,見後文信號量 bug 分析
else if (ws == 0 &&
!compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
continue; // loop on failed CAS
}
if (h == head) // loop if head changed
break;
}
}
}
之後 t4 (寫鎖被喚醒)在 acquireQueued 中 parkAndCheckInterrupt 處恢復運行,
再次 for (;;) 這次自己是老二,並且沒有其他競爭,tryAcquire(1) 成功,
修改頭結點,流程結束

在這裏插入圖片描述

3. ReentrantReadWriteLock鎖降級與升級

鎖降級:
由上面的源碼可以看出,線程在獲取讀鎖時,如果state!=0,那麼會先判斷獲取寫鎖的線程是不是當前線程,也就是說一個線程在獲取寫鎖後,還可以獲取讀鎖,當寫鎖釋放後,就降級爲讀鎖了。
在這裏插入圖片描述
不可以鎖升級:
在這裏插入圖片描述
在這裏插入圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章