文章目錄
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,那麼會先判斷獲取寫鎖的線程是不是當前線程,也就是說一個線程在獲取寫鎖後,還可以獲取讀鎖,當寫鎖釋放後,就降級爲讀鎖了。
不可以鎖升級: