針對讀多寫少的場景,Java提供了另外一個實現Lock接口的讀寫鎖ReentrantReadWriteLock(RRW),之前分析過ReentrantLock是一個獨佔鎖,同一時間只允許一個線程訪問。
而 RRW 允許多個讀線程同時訪問,但不允許寫線程和讀線程、寫線程和寫線程同時訪問。
讀寫鎖內部維護了兩個鎖,一個是用於讀操作的ReadLock,一個是用於寫操作的 WriteLock。
讀寫鎖遵守以下三條基本原則
允許多個線程同時讀共享變量;
只允許一個線程寫共享變量;
如果一個寫線程正在執行寫操作,此時禁止讀線程讀共享變量。
讀寫鎖如何實現
RRW也是基於AQS實現的,它的自定義同步器(繼承自AQS)需要在同步狀態state上維護多個讀線程和一個寫線程的狀態。RRW的做法是使用高低位來實現一個整形控制兩種狀態,一個int佔4個字節,一個字節8位。所以高16位表示讀,低16位表示寫。
abstract static class Sync extends AbstractQueuedSynchronizer {
static final int SHARED_SHIFT = 16;
// 10000000000000000(65536)
static final int SHARED_UNIT = (1 << SHARED_SHIFT);
// 65535
static final int MAX_COUNT = (1 << SHARED_SHIFT) - 1;
//1111111111111111
static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1;
// 讀鎖(共享鎖)的數量,只計算高16位的值
static int sharedCount(int c) { return c >>> SHARED_SHIFT; }
// 寫鎖(獨佔鎖)的數量
static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; }
}
獲取讀鎖
當線程獲取讀鎖時,首先判斷同步狀態低16位,如果存在寫鎖,則獲取鎖失敗,進入CLH隊列阻塞,反之,判斷當前線程是否應該被阻塞,如果不應該阻塞則嘗試 CAS 同步狀態,獲取成功更新同步鎖爲讀狀態。
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);
// 非公平鎖實現中readerShouldBlock()返回true表示CLH隊列中有正在排隊的寫鎖
// CAS設置讀鎖的狀態值
if (!readerShouldBlock() &&
r < MAX_COUNT &&
compareAndSetState(c, c + SHARED_UNIT)) {
// 省略記錄獲取readLock次數的代碼
return 1;
}
// 針對上面失敗的條件進行再次處理
return fullTryAcquireShared(current);
}
final int fullTryAcquireShared(Thread current) {
// 無線循環
for (;;) {
int c = getState();
if (exclusiveCount(c) != 0) {
// 如果不是當前線程持有寫鎖,則進入CLH隊列阻塞
if (getExclusiveOwnerThread() != current)
return -1;
}
// 如果reader應該被阻塞
else if (readerShouldBlock()) {
// Make sure we're not acquiring read lock reentrantly
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();
}
}
// 當前線程沒有持有讀鎖,即不存在鎖重入情況。則進入CLH隊列阻塞
if (rh.count == 0)
return -1;
}
}
// 共享鎖的如果超出了限制
if (sharedCount(c) == MAX_COUNT)
throw new Error("Maximum lock count exceeded");
// CAS設置狀態值
if (compareAndSetState(c, c + SHARED_UNIT)) {
// 省略記錄readLock次數的代碼
return 1;
}
}
}
SHARED_UNIT 的值是65536,也就是說,當第一次獲取讀鎖的後,state的值就變成了65536。
在公平鎖的實現中當CLH隊列中有排隊的線程, readerShouldBlock() 方法就會返回爲true。非公平鎖的實現中則是當CLH隊列中存在等待獲取寫鎖的線程就返回true
還需要注意的是獲取讀鎖的時候,如果當前線程已經持有寫鎖,是仍然能獲取讀鎖成功的。後面會提到鎖的降級,如果你對那裏的代碼有疑問,可以在回過頭來看看這裏申請鎖的代碼
釋放讀鎖
protected final boolean tryReleaseShared(int unused) {
for (;;) {
int c = getState();
// 減去65536
int nextc = c - SHARED_UNIT;
// 只有當state的值變成0纔會真正的釋放鎖
if (compareAndSetState(c, nextc))
return nextc == 0;
}
}
釋放鎖時,state的值需要減去65536,因爲當第一次獲取讀鎖後,state值變成了65536。
任何一個線程釋放讀鎖的時候只有在 state==0 的時候才真正釋放了鎖,比如有100個線程獲取了讀鎖,只有最後一個線程執行 tryReleaseShared 方法時才真正釋放了鎖,此時會喚醒CLH隊列中的排隊線程。
獲取寫鎖
一個線程嘗試獲取寫鎖時,會先判斷同步狀態 state 是否爲0。如果 state 等於 0,說明暫時沒有其它線程獲取鎖;如果 state 不等於 0,則說明有其它線程獲取了鎖。
此時再判斷state的低16位(w)是否爲0,如果w爲0,表示其他線程獲取了讀鎖,此時進入CLH隊列進行阻塞等待。
如果w不爲0,則說明其他線程獲取了寫鎖,此時需要判斷獲取了寫鎖的是不是當前線程,如果不是則進入CLH隊列進行阻塞等待,如果獲取了寫鎖的是當前線程,則判斷當前線程獲取寫鎖是否超過了最大次數,若超過,拋出異常。反之則更新同步狀態。
// 獲取寫鎖
protected final boolean tryAcquire(int acquires) {
Thread current = Thread.currentThread();
int c = getState();
int w = exclusiveCount(c);
// 判斷state是否爲0
if (c != 0) {
// 獲取鎖失敗
if (w == 0 || current != getExclusiveOwnerThread())
return false;
// 判斷當前線程獲取寫鎖是否超出了最大次數65535
if (w + exclusiveCount(acquires) > MAX_COUNT)
throw new Error("Maximum lock count exceeded");
// 鎖重入
setState(c + acquires);
return true;
}
// 非公平鎖實現中writerShouldBlock()永遠返回爲false
// CAS修改state的值
if (writerShouldBlock() ||
!compareAndSetState(c, c + acquires))
return false;
// CAS成功後,設置當前線程爲擁有獨佔鎖的線程
setExclusiveOwnerThread(current);
return true;
}
在公平鎖的實現中當CLH隊列中存在排隊的線程,那麼 writerShouldBlock() 方法就會返回爲true,此時獲取寫鎖的線程就會被阻塞。
釋放寫鎖
釋放寫鎖的邏輯比較簡單
protected final boolean tryRelease(int releases) {
// 寫鎖是否被當前線程持有
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
int nextc = getState() - releases;
boolean free = exclusiveCount(nextc) == 0;
// 沒有其他線程持有寫鎖
if (free)
setExclusiveOwnerThread(null);
setState(nextc);
return free;
}
鎖的升級?
// 準備讀緩存
readLock.lock();
try {
v = map.get(key);
if(v == null) {
writeLock.lock();
try {
if(map.get(key) != null) {
return map.get(key);
}
// 更新緩存代碼,省略
} finally {
writeLock.unlock();
}
}
} finally {
readLock.unlock();
}
對於上面獲取緩存數據(這也是RRW的應用場景)的代碼,先是獲取讀鎖,然後再升級爲寫鎖,這樣的行爲叫做鎖的升級。可惜RRW不支持,這樣會導致寫鎖永久等待,最終導致線程被永久阻塞。所以 鎖的升級是不允許的 。
鎖的降級
雖然鎖的升級不允許,但是鎖的降級卻是可以的。
ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
ReadLock readLock = lock.readLock();
WriteLock writeLock = lock.writeLock();
Map<String, String> dataMap = new HashMap();
public void processCacheData() {
readLock.lock();
if(!cacheValid()) {
// 釋放讀鎖,因爲不允許
readLock.unlock();
writeLock.lock();
try {
if(!cacheValid()) {
dataMap.put("key", "think123");
}
// 降級爲讀鎖
readLock.lock();
} finally {
writeLock.unlock();
}
}
try {
// 仍然持有讀鎖
System.out.println(dataMap);
} finally {
readLock.unlock();
}
}
public boolean cacheValid() {
return !dataMap.isEmpty();
}
RRW需要注意的問題
在讀取很多、寫入很少的情況下,RRW 會使寫入線程遭遇飢餓(Starvation)問題,也就是說寫入線程會因遲遲無法競爭到鎖而一直處於等待狀態。
寫鎖支持條件變量,讀鎖不支持。讀鎖調用newCondition() 會拋出UnsupportedOperationException 異常