前面我們學習了AQS,ReentrantLock等,現在來學習一下什麼是讀寫鎖ReentrantReadWriteLock。
當讀操作遠遠高於寫操作時,這時候可以使用【讀寫鎖】讓【讀-讀】可以併發,提高性能。
本文還是基於源碼的形式,希望同學們能夠以本文爲思路,自己跟蹤源碼一步步的debug進去,加深理解。
一、初識ReentrantReadWriteLock
同樣的,先看下其類圖:
- 實現了讀寫鎖接口
ReadWriteLock
- 有5個內部類,與ReentrantLock相同的是
FairSync
、NonfairSync
和Sync
,另外不同的是增加兩個內部類,都實現了Lock接口:WriteLock
ReadLock
- Sync 增加了兩個內部類 :
-
HoldCounter
:持有鎖的計數器 -
ThreadLocalHoldCounter
:維護HoldCounter的ThreadLocal
-
二、使用案例
通常會維護一個操作數據的容器類,內部應該封裝好數據的read和write方法,如下所示:
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantReadWriteLock;
/**
* @description: 數據容器類
* @author:weirx
* @date:2022/1/13 15:29
* @version:3.0
*/
public class DataContainer {
/**
* 初始化讀鎖和寫鎖
*/
private ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock();
private ReentrantReadWriteLock.ReadLock readLock = reentrantReadWriteLock.readLock();
private ReentrantReadWriteLock.WriteLock writeLock = reentrantReadWriteLock.writeLock();
protected void read(){
readLock.lock();
try {
System.out.println("獲取讀鎖");
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
readLock.unlock();
System.out.println("釋放讀鎖");
}
}
protected void write(){
writeLock.lock();
try {
System.out.println("獲取寫鎖");
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
writeLock.unlock();
System.out.println("釋放寫鎖");
}
}
}
簡單測試一下,分爲讀讀、讀寫、寫寫。
- 讀讀:
public static void main(String[] args) {
//初始化數據容器
DataContainer dataContainer = new DataContainer();
new Thread(() -> {
dataContainer.read();
}, "t1").start();
new Thread(() -> {
dataContainer.read();
}, "t2").start();
}
結果,讀讀不互斥,同時獲取讀鎖,同時釋放:
獲取讀鎖
獲取讀鎖
釋放讀鎖
釋放讀鎖
- 讀寫:
public static void main(String[] args) {
//初始化數據容器
DataContainer dataContainer = new DataContainer();
new Thread(() -> {
dataContainer.read();
}, "t1").start();
new Thread(() -> {
dataContainer.write();
}, "t2").start();
}
結果,讀寫互斥,無論是先執行read還是write方法,都會等到讀鎖或寫鎖被釋放之後,纔會獲取下一把鎖:
獲取讀鎖 -- 第一個執行
釋放讀鎖 -- 第二個執行
獲取寫鎖 -- 第三個執行
釋放寫鎖 -- 第四個執行
- 寫寫:
public static void main(String[] args) {
//初始化數據容器
DataContainer dataContainer = new DataContainer();
new Thread(() -> {
dataContainer.write();
}, "t1").start();
new Thread(() -> {
dataContainer.write();
}, "t2").start();
}
結果,寫寫互斥,只有第一把寫鎖釋放後,才能獲取下一把寫鎖:
獲取寫鎖
釋放寫鎖
獲取寫鎖
釋放寫鎖
注意:
- 鎖重入時,持有讀鎖再去獲取寫鎖,會導致寫鎖一直等待
結果:不會釋放protected void read(){ readLock.lock(); try { System.out.println("獲取讀鎖"); TimeUnit.SECONDS.sleep(1); System.out.println("獲取寫鎖"); writeLock.lock(); } catch (InterruptedException e) { e.printStackTrace(); } finally { readLock.unlock(); System.out.println("釋放讀鎖"); } }
獲取讀鎖 獲取寫鎖
- 鎖重入時,持有寫鎖,可以再去獲取讀鎖。
結果:protected void write(){ writeLock.lock(); try { System.out.println("獲取寫鎖"); TimeUnit.SECONDS.sleep(1); System.out.println("獲取讀鎖"); readLock.lock(); } catch (InterruptedException e) { e.printStackTrace(); } finally { writeLock.unlock(); System.out.println("釋放寫鎖"); } }
獲取寫鎖 獲取讀鎖 釋放寫鎖
三、源碼分析
我們根據前面的例子,從讀鎖的獲取到釋放,從寫鎖的獲取到釋放,依次查看源碼。
先注意一個事情,讀寫鎖是以不同的位數來區分獨佔鎖和共享鎖的狀態的:
/*
* 讀和寫分爲上行下兩個部分,低16位是獨佔鎖狀態,高16位是共享鎖狀態
*/
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;
/** 返回以count表示的共享持有數 */
static int sharedCount(int c) { return c >>> SHARED_SHIFT; }
/** 返回以count表示的互斥保持數 */
static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; }
3.1 讀鎖分析
3.1.1 讀鎖獲取
從 readLock.lock(); 這裏進入分析過程:
/**
* 獲取讀鎖。
* 如果寫鎖沒有被另一個線程持有,則獲取讀鎖並立即返回。
* 如果寫鎖被另一個線程持有,那麼當前線程將被禁用以用於線程調度目的並處於休眠狀態,直到獲得讀鎖爲止
*/
public void lock() {
sync.acquireShared(1);
}
如上的lock方法,是ReentrantReadWriteLock子類ReadLock的方法,而acquireShared方法是在AQS的子類Syn當中定義的,這個方法嘗試以共享的方式獲取讀鎖,失敗則進入等待隊列, 不斷重試,直到獲取讀鎖爲止。
public final void acquireShared(int arg) {
// 被其他線程持有的話,就走AQS的doAcquireShared
if (tryAcquireShared(arg) < 0)
// 獲取共享鎖,失敗加入等待隊列,不可中斷的獲取,直到獲取爲止
doAcquireShared(arg);
}
tryAcquireShared是在ReentrantReadWriteLock當中實現的,我們直接看代碼:
protected final int tryAcquireShared(int unused) {
// 獲取當前線程
Thread current = Thread.currentThread();
// 獲取當前鎖狀態
int c = getState();
// 獨佔鎖統計不等於0 且 持有者不是當前線程,就返回 -1 ,換句話說,被其他線程持有
if (exclusiveCount(c) != 0 &&
getExclusiveOwnerThread() != current)
return -1;
// 共享鎖數量
int r = sharedCount(c);
// 返回fase纔有資格獲取讀鎖
if (!readerShouldBlock() &&
// 持有數小於默認值
r < MAX_COUNT &&
// CAS 設置鎖狀態
compareAndSetState(c, c + SHARED_UNIT)) {
// 持有共享鎖爲0
if (r == 0) {
// 第一個持有者是當前線程
firstReader = current;
// 持有總數是 1
firstReaderHoldCount = 1;
} else if (firstReader == current) {
// 持有鎖的是當前線程本身,就把技術 + 1
firstReaderHoldCount++;
} else {
// 獲取緩存計數
HoldCounter rh = cachedHoldCounter;
// 如果是null 或者 持有線程的id不是當前線程
if (rh == null || rh.tid != getThreadId(current))
// 賦值給緩存
cachedHoldCounter = rh = readHolds.get();
else if (rh.count == 0)
// rh不是null ,且是當前線程,就把讀鎖持有者設爲緩存中的值
readHolds.set(rh);
// 將其 + 1
rh.count++;
}
return 1;
}
// 想要獲取讀鎖的線程應該被阻塞,保底工作,處理 CAS 未命中和在 tryAcquireShared 中未處理的重入讀取
return fullTryAcquireShared(current);
}
從上面的源碼我們可以看得出來,寫鎖和讀鎖之間是互斥的。
3.1.2 讀鎖釋放
直接看關鍵部分
/**
* 以共享模式釋放鎖,tryReleaseShared返回true,則釋放
*/
public final boolean releaseShared(int arg) {
// 釋放鎖
if (tryReleaseShared(arg)) {
// 喚醒隊列的下一個線程
doReleaseShared();
return true;
}
return false;
}
看看讀寫鎖的tryReleaseShared實現:
protected final boolean tryReleaseShared(int unused) {
//。。。省略。。。
for (;;) {
int c = getState();
int nextc = c - SHARED_UNIT;
if (compareAndSetState(c, nextc))
// 讀鎖的計數不會影響其它獲取讀鎖線程, 但會影響其它獲取寫鎖線程
// 計數爲 0 纔是真正釋放
return nextc == 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,用來解決傳播性
else if (ws == 0 &&
!compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
continue; // loop on failed CAS
}
if (h == head) // loop if head changed
break;
}
}
3.2 寫鎖分析
3.2.1 獲取鎖
public final void acquire(int arg) {
// 嘗試獲得寫鎖失敗
if (!tryAcquire(arg) &&
// 將當前線程關聯到一個 Node 對象上, 模式爲獨佔模式
// 進入 AQS 隊列阻塞
acquireQueued(addWaiter(Node.EXCLUSIVE), arg) ) {
selfInterrupt();
}
}
讀寫鎖的上鎖方法:tryAcquire
protected final boolean tryAcquire(int acquires) {
Thread current = Thread.currentThread();
int c = getState();
// 獲得低 16 位, 代表寫鎖的 state 計數
int w = exclusiveCount(c);
if (c != 0) {
// 如果寫鎖是0 或者 當前線程不等於獨佔線程,獲取失敗
if (w == 0 || 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.2.2 釋放鎖
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;
}
tryRelease:
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;
}