一、讀寫鎖簡介
現實中有這樣一種場景:對共享資源有讀和寫的操作,且寫操作沒有讀操作那麼頻繁。在沒有寫操作的時候,多個線程同時讀一個資源沒有任何問題,所以應該允許多個線程同時讀取共享資源;但是如果一個線程想去寫這些共享資源,就不應該允許其他線程對該資源進行讀和寫的操作了。
針對這種場景,JAVA的併發包提供了讀寫鎖ReentrantReadWriteLock,它表示兩個鎖,一個是讀操作相關的鎖,稱爲共享鎖;一個是寫相關的鎖,稱爲排他鎖
類圖如下:
說明:如上圖所示Sync爲ReentrantReadWriteLock內部類,Sync繼承自AQS、NonfairSync繼承自Sync類、FairSync繼承自Sync類(通過構造函數傳入的布爾值決定要構造哪一種Sync實例);ReadLock實現了Lock接口、WriteLock也實現了Lock接口;
AQS定義了獨佔模式的acquire()和release()方法,共享模式的acquireShared()和releaseShared()方法.還定義了抽象方法tryAcquire()、tryAcquiredShared()、tryRelease()和tryReleaseShared()由子類實現,tryAcquire()和tryAcquiredShared()分別對應獨佔模式和共享模式下的鎖的嘗試獲取,就是通過這兩個方法來實現公平性和非公平性,在嘗試獲取中,如果新來的線程必須先入隊才能獲取鎖就是公平的,否則就是非公平的。這裏可以看出AQS定義整體的同步器框架,具體實現放手交由子類實現。
通過類圖我們知道一些核心操作由Sync類實現
Sync類內部存在兩個內部類,分別爲HoldCounter和ThreadLocalHoldCounter,其中HoldCounter主要與讀鎖配套使用;
Sync源碼如下:
abstract static class Sync extends AbstractQueuedSynchronizer {
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; }
// 計數器
static final class HoldCounter {
// 計數
int count = 0;
// Use id, not reference, to avoid garbage retention
// 獲取當前線程的TID屬性的值
final long tid = getThreadId(Thread.currentThread());
}
// 本地線程計數器
static final class ThreadLocalHoldCounter
extends ThreadLocal<HoldCounter> {
// 重寫初始化方法,在沒有進行set的情況下,獲取的都是該HoldCounter值
public HoldCounter initialValue() {
return new HoldCounter();
}
}
// 本地線程計數器
private transient ThreadLocalHoldCounter readHolds;
// 緩存的計數器
private transient HoldCounter cachedHoldCounter;
/記錄第一個持有共享鎖線程的持有共享鎖的數量,作者認爲大多數情況下不會有併發,更多的是線程交替持有鎖
private transient Thread firstReader = null;
// 第一個讀線程的計數
private transient int firstReaderHoldCount;
//構造器
Sync() {
// 本地線程計數器
readHolds = new ThreadLocalHoldCounter();
// 設置AQS的狀態
setState(getState()); // ensures visibility of readHolds
}
}
直接上源碼
一.寫鎖過程
獲取寫鎖:
ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
ReentrantReadWriteLock.WriteLock writeLock = readWriteLock.writeLock();
writeLock.lock();
//ReentrantReadWriteLock 內部類Sync 繼承自AQS,這裏是調用aqs中的acquire方法
public void lock() {
sync.acquire(1);
}
//AQS中定義了tryAcquire抽象方法,具體的實現由子類去實現
//這裏除tryAcquire方法和Reentrantlock 略有不同,後續操作一樣一樣的,
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
說明:除了aqs中的tryAcquire由具體的實現類來實現,其他部分和ReetrantLock獲取鎖的過程一樣的,這裏就不絮叨了,下邊主要看下tryAcquire方法的具體實現。(可以參考我寫的這篇ReentrantLock詳解,或者不清楚的直接留言我)
protected final boolean tryAcquire(int acquires) {
Thread current = Thread.currentThread();
//獲取當前鎖對象狀態
int c = getState();
// 返回count中表示排它鎖的數量
int w = exclusiveCount(c);
//說明鎖被佔有(共享鎖或者排它鎖)
if (c != 0) {
// (Note: if c != 0 and w == 0 then shared count != 0)
//說明現在有共享鎖被別的線程佔有,嘗試獲取鎖失敗
if (w == 0 || current != getExclusiveOwnerThread())
return false;
//說明當前線程持有排它鎖或者共享鎖,這裏是判斷有沒有超出重入次數
if (w + exclusiveCount(acquires) > MAX_COUNT)
throw new Error("Maximum lock count exceeded");
// 排它鎖重入,直接獲取鎖
setState(c + acquires);
return true;
}
//如果當前爲非公平鎖: writerShouldBlock 方法直接返回 false,然後去爭搶鎖
/**如果當前爲公平的寫鎖 writerShouldBlock 該方法調動 AQS的hasQueuedPredecessors 方法,
判斷當前同步隊列有沒有等待的線程,如果有返回true,沒有等待的線程在返回false 然後去爭搶鎖**/
if (writerShouldBlock() ||
!compareAndSetState(c, c + acquires))
return false;
//成功獲取鎖,把當錢鎖設置爲當前線程佔有
setExclusiveOwnerThread(current);
return true;
}
這裏獲取鎖失敗的情況主要有
- 鎖被其他線程佔有(共享鎖或者排它鎖)
- 如果爲公平鎖,等待隊列上有線程等待
- 超出鎖重入最大數量
- 別的線程爭搶了鎖
失敗後的具體操作見ReentrantLock詳解
寫鎖的釋放
ReentrantReadWriteLock.WriteLock writeLock = readWriteLock.writeLock();
writeLock.unlock();
public void unlock() {
sync.release(1);
}
//AQS中定義了tryAcquire抽象方法,具體的實現由子類去實現
public final boolean release(int arg) {
//tryRelease嘗試釋放鎖(鎖status-arg),如果當前線程沒有佔有的鎖(鎖status=0) 返回true
if (tryRelease(arg)) {
//當前線程釋放掉了所有鎖
Node h = head;
//如果等待隊列第一個結點有掛起的線程,將它喚醒去爭搶
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
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;
}
讀鎖的獲取和釋放
讀鎖獲取過程:
ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
ReentrantReadWriteLock.ReadLock readLock = readWriteLock.readLock();
readLock.lock();
public void lock() {
sync.acquireShared(1);
}
public final void acquireShared(int arg) {
if (tryAcquireShared(arg) < 0)
doAcquireShared(arg);
}
主要來看下aqs定義的抽象方法tryAcquireShared (sync具體實現的)
獲取讀鎖失敗的情況有 :
(1)有其他線程持有排它鎖,獲取鎖失敗。
(2)公平鎖:同步隊列有等待節點;非公平鎖:同步隊列頭節點爲排它鎖同步隊列(防止寫鎖飢餓)
(3)讀鎖數量達到最多,拋出異常。
除了以上三種情況,該線程會循環嘗試獲取讀鎖直到成功。
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,有可能是共享鎖也有可能是排它鎖的節點,
//如果是非公平鎖:同步隊列第一個節點是等待排它鎖 就返回true,防止排它鎖出現飢餓狀態
//readerShouldBlock 爲false就直接獲取鎖
if (!readerShouldBlock() &&
//c +的是 1<<16,讀鎖爲高16位表示
r < MAX_COUNT &&
compareAndSetState(c, c + SHARED_UNIT)) {
//r == 0,表示第一個讀鎖線程,第一個讀鎖firstRead是不會加入到readHolds中
if (r == 0) {
// 設置第一個讀線程
firstReader = current;
// 讀線程佔用的資源數爲1
firstReaderHoldCount = 1;
} else if (firstReader == current) {// 當前線程爲第一個讀線程,表示第一個讀鎖線程重入
// 佔用資源數加1
firstReaderHoldCount++;
} else {
// 獲取計數器
//如果共享鎖是被第2+n個線程佔有,則使用threadlocal 記錄每個線程持有的線程數量
HoldCounter rh = cachedHoldCounter;
// 計數器爲空或者計數器的tid不爲當前正在運行的線程的tid
if (rh == null || rh.tid != getThreadId(current))
// 獲取當前線程對應的計數器
cachedHoldCounter = rh = readHolds.get();
else if (rh.count == 0) // 計數爲0
//加入到readHolds中
readHolds.set(rh);
//計數+1
rh.count++;
}
return 1;
}
//獲取鎖失敗,放到循環裏重試
return fullTryAcquireShared(current);
}
final int fullTryAcquireShared(Thread current) {
HoldCounter rh = null;
for (;;) {
int c = getState();
//有線程持有寫鎖,且該線程不是當前線程,獲取鎖失敗
if (exclusiveCount(c) != 0) {
if (getExclusiveOwnerThread() != current)
return -1;
else{
//有線程持有寫鎖,且該線程是當前線程,則應該放行讓其重入獲取鎖,否則會造成死鎖
}
//沒有線程持有排它鎖,判斷獲取共享鎖是否應該被阻塞
//readerShouldBlock 方法 判斷同步隊列中第一個節點是 什麼狀態
//如果是公平鎖:同步隊列有節點就返回true,有可能是共享鎖也有可能是排它鎖的節點,
//如果是非公平鎖:同步隊列第一個節點是等待排它鎖 就返回true,防止排它鎖出現飢餓狀態
//readerShouldBlock 爲false就直接獲取鎖
} else if (readerShouldBlock()) {
// 確保獲取的不是 讀重入鎖
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");
//再次嘗試獲取鎖~~(可能爲寫讀重入)
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;
}
}
}
讀鎖獲取失敗,調用aqs的doAcquireShared方法嘗試將當前線程任務節點加入到同步隊列中(加入同步隊列的具體細節見ReentrantLock詳解)
private void doAcquireShared(int arg) {
// 將當前線程任務添加到同步隊列中
final Node node = addWaiter(Node.SHARED);
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
// 獲取當前節點的前繼節點
final Node p = node.predecessor();
// 判斷前繼節點是否是head節點
if (p == head) {
//如果前置節點爲head說明,他當前線程是等待隊列中的第一個,那麼就嘗試獲取鎖(這裏可能是避免線程的上下文切換)
int r = tryAcquireShared(arg);
if (r >= 0) {
// 獲取 lock 成功, 設置新的 head, 並喚醒後繼獲取 readLock 的節點
setHeadAndPropagate(node, r);
p.next = null; // help GC
// 該線程有可能是被中斷喚醒,也有可能是被其他線程喚醒,這裏設置下中斷狀態
if (interrupted)
selfInterrupt();
failed = false;
return;
}
}
//普通鎖的情況下:,然後返回false繼續自旋 嘗試獲取鎖
//shouldParkAfterFailedAcquire 只有發現當前節點不是首節點纔會返回true ,然後掛起當前線程,
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
//如果該線程是被中斷喚醒的,用於輔助後續操作判斷當前線程是被中斷喚醒的
interrupted = true;
}
} finally {
//如果該方法因爲某些特殊情況意外的退出(沒有獲取鎖就退出了),那麼就取消嘗試獲取鎖
if (failed)
cancelAcquire(node);
}
}
如果讀鎖獲取失敗後,嘗試將當前線程節點加入到同步隊列中。
如果該節點爲頭節點,那麼就自旋爭搶鎖(避免上下文切換),獲取鎖成功的話,調用setHeadAndPropagate方法繼續喚醒後續節點(如果後續節點爲讀鎖等待節點的話);
如果該節點不爲頭節點,將當前線程掛起;
我們來看下setHeadAndPropagate方法
// 如果讀鎖(共享鎖)獲取成功,或頭部節點爲空,或頭節點取消,或剛獲取讀鎖的線程的下一個節點爲空,或在節點的下個節點也在申請讀鎖,
//則在CLH隊列中傳播下去喚醒線程
private void setHeadAndPropagate(Node node, int propagate) {
Node h = head; // Record old head for check below
setHead(node);
if (propagate > 0 || h == null || h.waitStatus < 0 ||
(h = head) == null || h.waitStatus < 0) {
Node s = node.next;
if (s == null || s.isShared())
//下面具體分析
doReleaseShared();
}
}
注:後續讀鎖的釋放操作也調用的這個doReleaseShared 方法
private void doReleaseShared() {
for (;;) {
//喚醒操作由頭結點開始,注意這裏的頭節點已經是上面新設置的頭結點了
//其實就是喚醒上面新獲取到共享鎖的節點的後繼節點
Node h = head;
if (h != null && h != tail) {
int ws = h.waitStatus;
//表示後繼節點需要被喚醒
if (ws == Node.SIGNAL) {
//這裏需要控制併發,因爲入口有setHeadAndPropagate跟releaseShared兩個,避免兩次unpark
if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
continue;
//執行喚醒操作
unparkSuccessor(h);
}
//如果後繼節點暫時不需要喚醒,則把當前節點狀態設置爲PROPAGATE
(這裏不是很明白,爲什麼不需要喚醒的節點要設置這個狀態,哪個老鐵知道爲什麼的話指點下)
else if (ws == 0 &&
!compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
continue;
}
//如果頭結點沒有發生變化,表示設置完成,退出循環
//如果頭結點發生變化,比如說其他線程獲取到了鎖,爲了使自己的喚醒動作可以傳遞,必須進行重試
if (h == head)
break;
}
怎麼理解這個傳播呢:
就是隻要獲取成功到讀鎖,那就要傳播到下一個節點(如果一下個節點繼續是讀鎖的申請,只要成功獲取,就再下一個節點,直到隊列尾部或爲寫鎖的申請,停止傳播)。
讀鎖的釋放過程
ReentrantReadWriteLock.ReadLock readLock = readWriteLock.readLock();
readLock.unlock();
public void unlock() {
sync.releaseShared(1);
}
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
//上文有講
doReleaseShared();
return true;
}
return false;
}
//該方法的主要作用就是用來維護下當前線程讀鎖的重入數量;
//如果沒有線程佔有讀鎖,就返回true 喚醒後續節點
protected final boolean tryReleaseShared(int unused) {
Thread current = Thread.currentThread();
if (firstReader == current) {
// assert firstReaderHoldCount > 0;
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();
if (count <= 0)
throw unmatchedUnlockException();
}
--rh.count;
}
//保證一定能釋放掉讀鎖的佔有
for (;;) {
int c = getState();
int nextc = c - SHARED_UNIT;
if (compareAndSetState(c, nextc))
// Releasing the read lock has no effect on readers,
// but it may allow waiting writers to proceed if
// both read and write locks are now free.
return nextc == 0;
}
}
讀鎖的釋放過程比較簡單,這裏就不做過多的解釋了
這裏思考一個問題,獲取讀鎖的時候我們講到讀鎖傳播的概念,爲什麼在讀鎖釋放的時候,如果還有別的線程佔有讀鎖就不用傳播了呢?
因爲在現在獲取讀鎖的時候 已經完成讀線程喚醒的傳播了~~
總結:
獲取寫鎖:獲取寫鎖的過程總體和ReentrantLock詳解流程一樣;
- 嘗試獲取寫鎖(如果沒有線程佔有鎖直接獲取成功,並把當前鎖設爲獨佔)
-
公平鎖:先判斷同步隊列中是否有等待節點在等待獲取鎖
-
非公平鎖:上來就直接爭搶鎖
-
- 如果有其他現在佔有鎖(讀鎖或者寫鎖),獲取失敗,加入到同步隊列中
- 判斷當前節點是否爲頭節點,
- 是:自旋爭搶鎖(避免上下文切換)
- 否:調用LockSupport.park 方法掛起當前線程,等待被喚醒(別的線程調用LockSupport.uppark 或者被中斷 或者 超時)
寫鎖的釋放:
- 判斷當前鎖是否爲釋放鎖的線程佔有
- 設置當前鎖狀態,如果所有重入鎖都釋放掉了,設置當前鎖獨佔爲null,
- 喚醒同步隊列中的頭節點
注:寫鎖的釋放並沒有進行讀鎖的釋放傳播,讀鎖的傳播是有讀鎖成功獲取讀鎖以後進行的
讀鎖的獲取:
- 判斷寫鎖是否被佔用(是:判斷是否爲寫讀重入鎖)
- 是:判斷是否爲寫讀重入鎖
- 否:獲取失敗,加入同步隊列中
- 是:判斷是否爲寫讀重入鎖
- 判斷當前讀線程是否應該被阻塞
- 公平鎖:如果同步隊列中有等待節點就獲取鎖失敗,把當前讀線程節點加入到同步隊列
- 非公平鎖:如果同步隊列中的第一個節點爲寫鎖的等待節點獲取鎖失敗,把當前讀線程節點加入到同步隊列(防止寫鎖飢餓)
- 獲取讀鎖成功,維護線程佔有讀鎖的數量,判斷當前節點是否爲第一個獲取讀鎖的線程:
- 否:把當前線程佔有讀鎖數量維護進入HoldCounter(繼承自ThreadLocal,爲每個線程都維護了一個讀鎖重入的計數)
- 是:直接通過變量firstReader,firstReaderHoldCount維護當前線程佔有讀鎖的數量(這裏作者應該是認爲大多數情況下鎖的獲取爲交替獲取,沒必要直接就用線程計數器來爲每個線程爲一個數量)
- 注:如果後續節點爲讀鎖節點,就喚醒(該行爲會傳播)
- 獲取鎖失敗:加入到同步隊列中,判斷當前節點是否頭節點
- 是:自旋爭搶鎖
- 否:掛起當前線程,等待被喚醒
讀鎖的釋放:
- 設置當前線程佔有的鎖數量-1
- 喚醒後續節點~~~