1. 讀寫鎖簡介
現實中有這樣一種場景:對共享資源有讀和寫的操作,且寫操作沒有讀操作那麼頻繁。在沒有寫操作的時候,多個線程同時讀一個資源沒有任何問題,所以應該允許多個線程同時讀取共享資源;但是如果一個線程想去寫這些共享資源,就不應該允許其他線程對該資源進行讀和寫的操作了。
針對這種場景,JAVA的併發包提供了讀寫鎖ReentrantReadWriteLock,它表示兩個鎖,一個是讀操作相關的鎖,稱爲共享鎖;一個是寫相關的鎖,稱爲排他鎖,描述如下:
線程進入讀鎖的前提條件:
沒有其他線程的寫鎖,
沒有寫請求或者有寫請求,但調用線程和持有鎖的線程是同一個。
線程進入寫鎖的前提條件:
沒有其他線程的讀鎖
沒有其他線程的寫鎖
而讀寫鎖有以下三個重要的特性
(1)公平選擇性:支持非公平(默認)和公平的鎖獲取方式,吞吐量還是非公平優於公平。
(2)重進入:讀鎖和寫鎖都支持線程重進入。
(3)鎖降級:遵循獲取寫鎖、獲取讀鎖再釋放寫鎖的次序,寫鎖能夠降級成爲讀鎖。
2. 源碼解讀
我們先來看下 ReentrantReadWriteLock 類的整體結構:
public class ReentrantReadWriteLock implements ReadWriteLock, java.io.Serializable {
/** 讀鎖 */
private final ReentrantReadWriteLock.ReadLock readerLock;
/** 寫鎖 */
private final ReentrantReadWriteLock.WriteLock writerLock;
final Sync sync;
/** 使用默認(非公平)的排序屬性創建一個新的 ReentrantReadWriteLock */
public ReentrantReadWriteLock() {
this(false);
}
/** 使用給定的公平策略創建一個新的 ReentrantReadWriteLock */
public ReentrantReadWriteLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
readerLock = new ReadLock(this);
writerLock = new WriteLock(this);
}
/** 返回用於寫入操作的鎖 */
public ReentrantReadWriteLock.WriteLock writeLock() { return writerLock; }
/** 返回用於讀取操作的鎖 */
public ReentrantReadWriteLock.ReadLock readLock() { return readerLock; }
abstract static class Sync extends AbstractQueuedSynchronizer {}
static final class NonfairSync extends Sync {}
static final class FairSync extends Sync {}
public static class ReadLock implements Lock, java.io.Serializable {}
public static class WriteLock implements Lock, java.io.Serializable {}
}
類的繼承關係
public class ReentrantReadWriteLock
implements ReadWriteLock, java.io.Serializable {}
說明:可以看到,ReentrantReadWriteLock實現了ReadWriteLock接口,ReadWriteLock接口定義了獲取讀鎖和寫鎖的規範,具體需要實現類去實現;同時其還實現了Serializable接口,表示可以進行序列化,在源代碼中可以看到ReentrantReadWriteLock實現了自己的序列化邏輯。
類的內部類
ReentrantReadWriteLock有五個內部類,五個內部類之間也是相互關聯的。內部類的關係如下圖所示。
說明:如上圖所示,Sync繼承自AQS、NonfairSync繼承自Sync類、FairSync繼承自Sync類(通過構造函數傳入的布爾值決定要構造哪一種Sync實例);ReadLock實現了Lock接口、WriteLock也實現了Lock接口。
Sync類:
(1)類的繼承關係
abstract static class Sync extends AbstractQueuedSynchronizer {}
說明:Sync抽象類繼承自AQS抽象類,Sync類提供了對ReentrantReadWriteLock的支持。
(2)類的內部類
Sync類內部存在兩個內部類,分別爲HoldCounter和ThreadLocalHoldCounter,其中HoldCounter主要與讀鎖配套使用,其中,HoldCounter源碼如下。
// 計數器
static final class HoldCounter {
// 計數
int count = 0;
// Use id, not reference, to avoid garbage retention
// 獲取當前線程的TID屬性的值
final long tid = getThreadId(Thread.currentThread());
}
說明:HoldCounter主要有兩個屬性,count和tid,其中count表示某個讀線程重入的次數,tid表示該線程的tid字段的值,該字段可以用來唯一標識一個線程。ThreadLocalHoldCounter的源碼如下
// 本地線程計數器
static final class ThreadLocalHoldCounter
extends ThreadLocal<HoldCounter> {
// 重寫初始化方法,在沒有進行set的情況下,獲取的都是該HoldCounter值
public HoldCounter initialValue() {
return new HoldCounter();
}
}
說明:ThreadLocalHoldCounter重寫了ThreadLocal的initialValue方法,ThreadLocal類可以將線程與對象相關聯。在沒有進行set的情況下,get到的均是initialValue方法裏面生成的那個HolderCounter對象。
(3)類的屬性
abstract static class Sync extends AbstractQueuedSynchronizer {
// 版本序列號
private static final long serialVersionUID = 6317671515068378041L;
// 高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;
// 本地線程計數器
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
}
說明:在Sync的構造函數中設置了本地線程計數器和AQS的狀態state。
讀寫狀態的設計
同步狀態在重入鎖的實現中是表示被同一個線程重複獲取的次數,即一個整形變量來維護,但是之前的那個表示僅僅表示是否鎖定,而不用區分是讀鎖還是寫鎖。而讀寫鎖需要在同步狀態(一個整形變量)上維護多個讀線程和一個寫線程的狀態。
讀寫鎖對於同步狀態的實現是在一個整形變量上通過“按位切割使用”:將變量切割成兩部分,高16位表示讀,低16位表示寫。
假設當前同步狀態值爲S,get和set的操作如下:
(1)獲取寫狀態:
S&0x0000FFFF:將高16位全部抹去
(2)獲取讀狀態:
S>>>16:無符號補0,右移16位
(3)寫狀態加1:
S+1
(4)讀狀態加1:
S+(1<<16)即S + 0x00010000
在代碼層的判斷中,如果S不等於0,當寫狀態(S&0x0000FFFF),而讀狀態(S>>>16)大於0,則表示該讀寫鎖的讀鎖已被獲取。
寫鎖的獲取與釋放
public void lock() {
sync.acquire(1);
}
public void unlock() {
sync.release(1);
}
可以看到就是調用的獨佔式同步狀態的獲取與釋放,因此真實的實現就是Sync的 tryAcquire和 tryRelease。
寫鎖的獲取,看下tryAcquire:
protected final boolean tryAcquire(int acquires) {
//當前線程
Thread current = Thread.currentThread();
//獲取狀態
int c = getState();
//寫線程數量(即獲取獨佔鎖的重入數)
int w = exclusiveCount(c);
//當前同步狀態state != 0,說明已經有其他線程獲取了讀鎖或寫鎖
if (c != 0) {
// 當前state不爲0,此時:如果寫鎖狀態爲0說明讀鎖此時被佔用返回false;
// 如果寫鎖狀態不爲0且寫鎖沒有被當前線程持有返回false
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;
}
//到這裏說明此時c=0,讀鎖和寫鎖都沒有被獲取
//writerShouldBlock表示是否阻塞
if (writerShouldBlock() ||
!compareAndSetState(c, c + acquires))
return false;
//設置鎖爲當前線程所有
setExclusiveOwnerThread(current);
return true;
}
其中exclusiveCount方法表示佔有寫鎖的線程數量,源碼如下:
static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; }
說明:直接將狀態state和(2^16 - 1)做與運算,其等效於將state模上2^16。寫鎖數量由state的低十六位表示。
從源代碼可以看出,獲取寫鎖的步驟如下:
(1)首先獲取c、w。c表示當前鎖狀態;w表示寫線程數量。然後判斷同步狀態state是否爲0。如果state!=0,說明已經有其他線程獲取了讀鎖或寫鎖,執行(2);否則執行(5)。
(2)如果鎖狀態不爲零(c != 0),而寫鎖的狀態爲0(w = 0),說明讀鎖此時被其他線程佔用,所以當前線程不能獲取寫鎖,自然返回false。或者鎖狀態不爲零,而寫鎖的狀態也不爲0,但是獲取寫鎖的線程不是當前線程,則當前線程也不能獲取寫鎖。
(3)判斷當前線程獲取寫鎖是否超過最大次數,若超過,拋異常,反之更新同步狀態(此時當前線程已獲取寫鎖,更新是線程安全的),返回true。
(4)如果state爲0,此時讀鎖或寫鎖都沒有被獲取,判斷是否需要阻塞(公平和非公平方式實現不同),在非公平策略下總是不會被阻塞,在公平策略下會進行判斷(判斷同步隊列中是否有等待時間更長的線程,若存在,則需要被阻塞,否則,無需阻塞),如果不需要阻塞,則CAS更新同步狀態,若CAS成功則返回true,失敗則說明鎖被別的線程搶去了,返回false。如果需要阻塞則也返回false。
(5)成功獲取寫鎖後,將當前線程設置爲佔有寫鎖的線程,返回true。
方法流程圖如下:
寫鎖的釋放,tryRelease方法:
protected final boolean tryRelease(int releases) {
//若鎖的持有者不是當前線程,拋出異常
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
//寫鎖的新線程數
int nextc = getState() - releases;
//如果獨佔模式重入數爲0了,說明獨佔模式被釋放
boolean free = exclusiveCount(nextc) == 0;
if (free)
//若寫鎖的新線程數爲0,則將鎖的持有者設置爲null
setExclusiveOwnerThread(null);
//設置寫鎖的新線程數
//不管獨佔模式是否被釋放,更新獨佔重入數
setState(nextc);
return free;
}
寫鎖的釋放過程還是相對而言比較簡單的:首先查看當前線程是否爲寫鎖的持有者,如果不是拋出異常。然後檢查釋放後寫鎖的線程數是否爲0,如果爲0則表示寫鎖空閒了,釋放鎖資源將鎖的持有線程設置爲null,否則釋放僅僅只是一次重入鎖而已,並不能將寫鎖的線程清空。
說明:此方法用於釋放寫鎖資源,首先會判斷該線程是否爲獨佔線程,若不爲獨佔線程,則拋出異常,否則,計算釋放資源後的寫鎖的數量,若爲0,表示成功釋放,資源不將被佔用,否則,表示資源還被佔用。其方法流程圖如下。
讀鎖的獲取與釋放
類似於寫鎖,讀鎖的lock和unlock的實際實現對應Sync的 tryAcquireShared 和 tryReleaseShared方法。
讀鎖的獲取,看下tryAcquireShared方法
protected final int tryAcquireShared(int unused) {
// 獲取當前線程
Thread current = Thread.currentThread();
// 獲取狀態
int c = getState();
//如果寫鎖線程數 != 0 ,且獨佔鎖不是當前線程則返回失敗,因爲存在鎖降級
if (exclusiveCount(c) != 0 &&
getExclusiveOwnerThread() != current)
return -1;
// 讀鎖數量
int r = sharedCount(c);
/*
* readerShouldBlock():讀鎖是否需要等待(公平鎖原則)
* r < MAX_COUNT:持有線程小於最大數(65535)
* compareAndSetState(c, c + SHARED_UNIT):設置讀取鎖狀態
*/
// 讀線程是否應該被阻塞、並且小於最大值、並且比較設置成功
if (!readerShouldBlock() &&
r < MAX_COUNT &&
compareAndSetState(c, c + SHARED_UNIT)) {
//r == 0,表示第一個讀鎖線程,第一個讀鎖firstRead是不會加入到readHolds中
if (r == 0) { // 讀鎖數量爲0
// 設置第一個讀線程
firstReader = current;
// 讀線程佔用的資源數爲1
firstReaderHoldCount = 1;
} else if (firstReader == current) { // 當前線程爲第一個讀線程,表示第一個讀鎖線程重入
// 佔用資源數加1
firstReaderHoldCount++;
} else { // 讀鎖數量不爲0並且不爲當前線程
// 獲取計數器
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);
}
其中sharedCount方法表示佔有讀鎖的線程數量,源碼如下:
static int sharedCount(int c) { return c >>> SHARED_SHIFT; }
說明:直接將state右移16位,就可以得到讀鎖的線程數量,因爲state的高16位表示讀鎖,對應的第十六位表示寫鎖數量。
讀鎖獲取鎖的過程比寫鎖稍微複雜些,首先判斷寫鎖是否爲0並且當前線程不佔有獨佔鎖,直接返回;否則,判斷讀線程是否需要被阻塞並且讀鎖數量是否小於最大值並且比較設置狀態成功,若當前沒有讀鎖,則設置第一個讀線程firstReader和firstReaderHoldCount;若當前線程線程爲第一個讀線程,則增加firstReaderHoldCount;否則,將設置當前線程對應的HoldCounter對象的值。流程圖如下。
注意:更新成功後會在firstReaderHoldCount中或readHolds(ThreadLocal類型的)的本線程副本中記錄當前線程重入數(23行至43行代碼),這是爲了實現jdk1.6中加入的getReadHoldCount()方法的,這個方法能獲取當前線程重入共享鎖的次數(state中記錄的是多個線程的總重入次數),加入了這個方法讓代碼複雜了不少,但是其原理還是很簡單的:如果當前只有一個線程的話,還不需要動用ThreadLocal,直接往firstReaderHoldCount這個成員變量裏存重入數,當有第二個線程來的時候,就要動用ThreadLocal變量readHolds了,每個線程擁有自己的副本,用來保存自己的重入數。
fullTryAcquireShared方法:
final int fullTryAcquireShared(Thread current) {
HoldCounter rh = null;
for (;;) { // 無限循環
// 獲取狀態
int c = getState();
if (exclusiveCount(c) != 0) { // 寫線程數量不爲0
if (getExclusiveOwnerThread() != current) // 不爲當前線程
return -1;
} else if (readerShouldBlock()) { // 寫線程數量爲0並且讀線程被阻塞
// 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)) { // 計數器爲空或者計數器的tid不爲當前正在運行的線程的tid
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) { // 讀線程數量爲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;
}
}
}
說明:在tryAcquireShared函數中,如果下列三個條件不滿足(讀線程是否應該被阻塞、小於最大值、比較設置成功)則會進行fullTryAcquireShared函數中,它用來保證相關操作可以成功。其邏輯與tryAcquireShared邏輯類似,不再累贅。
讀鎖的釋放,tryReleaseShared方法
protected final boolean tryReleaseShared(int unused) {
// 獲取當前線程
Thread current = Thread.currentThread();
if (firstReader == current) { // 當前線程爲第一個讀線程
// assert firstReaderHoldCount > 0;
if (firstReaderHoldCount == 1) // 讀線程佔用的資源數爲1
firstReader = null;
else // 減少佔用的資源
firstReaderHoldCount--;
} else { // 當前線程不爲第一個讀線程
// 獲取緩存的計數器
HoldCounter rh = cachedHoldCounter;
if (rh == null || rh.tid != getThreadId(current)) // 計數器爲空或者計數器的tid不爲當前正在運行的線程的tid
// 獲取當前線程對應的計數器
rh = readHolds.get();
// 獲取計數
int count = rh.count;
if (count <= 1) { // 計數小於等於1
// 移除
readHolds.remove();
if (count <= 0) // 計數小於等於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;
}
}
說明:此方法表示讀鎖線程釋放鎖。首先判斷當前線程是否爲第一個讀線程firstReader,若是,則判斷第一個讀線程佔有的資源數firstReaderHoldCount是否爲1,若是,則設置第一個讀線程firstReader爲空,否則,將第一個讀線程佔有的資源數firstReaderHoldCount減1;若當前線程不是第一個讀線程,那麼首先會獲取緩存計數器(上一個讀鎖線程對應的計數器 ),若計數器爲空或者tid不等於當前線程的tid值,則獲取當前線程的計數器,如果計數器的計數count小於等於1,則移除當前線程對應的計數器,如果計數器的計數count小於等於0,則拋出異常,之後再減少計數即可。無論何種情況,都會進入無限循環,該循環可以確保成功設置狀態state。其流程圖如下。
在讀鎖的獲取、釋放過程中,總是會有一個對象存在着,同時該對象在獲取線程獲取讀鎖是+1,釋放讀鎖時-1,該對象就是HoldCounter。
要明白HoldCounter就要先明白讀鎖。前面提過讀鎖的內在實現機制就是共享鎖,對於共享鎖其實我們可以稍微的認爲它不是一個鎖的概念,它更加像一個計數器的概念。一次共享鎖操作就相當於一次計數器的操作,獲取共享鎖計數器+1,釋放共享鎖計數器-1。只有當線程獲取共享鎖後才能對共享鎖進行釋放、重入操作。所以HoldCounter的作用就是當前線程持有共享鎖的數量,這個數量必須要與線程綁定在一起,否則操作其他線程鎖就會拋出異常。
先看讀鎖獲取鎖的部分:
if (r == 0) {//r == 0,表示第一個讀鎖線程,第一個讀鎖firstRead是不會加入到readHolds中
firstReader = current;
firstReaderHoldCount = 1;
} else if (firstReader == current) {//第一個讀鎖線程重入
firstReaderHoldCount++;
} else { //非firstReader計數
HoldCounter rh = cachedHoldCounter;//readHoldCounter緩存
//rh == null 或者 rh.tid != current.getId(),需要獲取rh
if (rh == null || rh.tid != current.getId())
cachedHoldCounter = rh = readHolds.get();
else if (rh.count == 0)
readHolds.set(rh); //加入到readHolds中
rh.count++; //計數+1
}
這裏爲什麼要搞一個firstRead、firstReaderHoldCount呢?而不是直接使用else那段代碼?這是爲了一個效率問題,firstReader是不會放入到readHolds中的,如果讀鎖僅有一個的情況下就會避免查找readHolds。可能就看這個代碼還不是很理解HoldCounter。我們先看firstReader、firstReaderHoldCount的定義:
private transient Thread firstReader = null;
private transient int firstReaderHoldCount;
這兩個變量比較簡單,一個表示線程,當然該線程是一個特殊的線程,一個是firstReader的重入計數。
HoldCounter的定義:
static final class HoldCounter {
int count = 0;
final long tid = Thread.currentThread().getId();
}
在HoldCounter中僅有count和tid兩個變量,其中count代表着計數器,tid是線程的id。但是如果要將一個對象和線程綁定起來僅記錄tid肯定不夠的,而且HoldCounter根本不能起到綁定對象的作用,只是記錄線程tid而已。
誠然,在java中,我們知道如果要將一個線程和對象綁定在一起只有ThreadLocal才能實現。所以如下:
static final class ThreadLocalHoldCounter
extends ThreadLocal<HoldCounter> {
public HoldCounter initialValue() {
return new HoldCounter();
}
}
故而,HoldCounter應該就是綁定線程上的一個計數器,而ThradLocalHoldCounter則是線程綁定的ThreadLocal。從上面我們可以看到ThreadLocal將HoldCounter綁定到當前線程上,同時HoldCounter也持有線程Id,這樣在釋放鎖的時候才能知道ReadWriteLock裏面緩存的上一個讀取線程(cachedHoldCounter)是否是當前線程。這樣做的好處是可以減少ThreadLocal.get()的次數,因爲這也是一個耗時操作。需要說明的是這樣HoldCounter綁定線程id而不綁定線程對象的原因是避免HoldCounter和ThreadLocal互相綁定而GC難以釋放它們(儘管GC能夠智能的發現這種引用而回收它們,但是這需要一定的代價),所以其實這樣做只是爲了幫助GC快速回收對象而已。
3. 總結
通過上面的源碼分析,我們可以發現一個現象:
在線程持有讀鎖的情況下,該線程不能取得寫鎖(因爲獲取寫鎖的時候,如果發現當前的讀鎖被佔用,就馬上獲取失敗,不管讀鎖是不是被當前線程持有)。
在線程持有寫鎖的情況下,該線程可以繼續獲取讀鎖(獲取讀鎖時如果發現寫鎖被佔用,只有寫鎖沒有被當前線程佔用的情況纔會獲取失敗)。
仔細想想,這個設計是合理的:因爲當線程獲取讀鎖的時候,可能有其他線程同時也在持有讀鎖,因此不能把獲取讀鎖的線程“升級”爲寫鎖;而對於獲得寫鎖的線程,它一定獨佔了讀寫鎖,因此可以繼續讓它獲取讀鎖,當它同時獲取了寫鎖和讀鎖後,還可以先釋放寫鎖繼續持有讀鎖,這樣一個寫鎖就“降級”爲了讀鎖。
綜上:
一個線程要想同時持有寫鎖和讀鎖,必須先獲取寫鎖再獲取讀鎖;寫鎖可以“降級”爲讀鎖;讀鎖不能“升級”爲寫鎖。