一、前言
上一篇Java鎖之ReentrantLock(二)分析了ReentrantLock實現(xiàn)利器AQS同步器搀突,通過AQS源碼分析蛀恩,我們知道了同步器通過sate狀態(tài)進行鎖的獲取與釋放谢鹊,同時構(gòu)造了雙向FIFO雙向鏈表進行線程節(jié)點的等待,線程節(jié)點通過waitStatus來判斷自己需要掛起還是喚醒去獲取鎖哥桥。那么接下來我們繼續(xù)分析ReentrantLock的讀寫鎖,ReentrantReadWriteLock鎖激涤。
二拟糕、ReentrantReadWriteLock總覽
ReentrantReadWriteLock鎖 實際也是繼承了AQS類來實現(xiàn)鎖的功能的,上一篇Java鎖之ReentrantLock(二)已經(jīng)詳細解析過AQS的實現(xiàn),如果已經(jīng)掌握了AQS的原理送滞,相信接下來的讀寫鎖的解析也非常容易侠草。
- ReentrantReadWriteLock鎖內(nèi)部類列表
類 | 作用 |
---|---|
Sync, | 繼承AQS犁嗅,鎖功能的主要實現(xiàn)者 |
FairSync | 繼承Sync边涕,主要實現(xiàn)公平鎖 |
NofairSync | 繼承Sync,主要實現(xiàn)非公平鎖 |
ReadLock | 讀鎖愧哟,通過sync代理實現(xiàn)鎖功能 |
WriteLock | 寫鎖奥吩,通過sync代理實現(xiàn)鎖功能 |
我們先分析讀寫鎖中的這4個int 常量,其實這4個常量的作用就是區(qū)分一個int整數(shù)的高16位和低16位的蕊梧,ReentrantReadWriteLock鎖還是依托于state變量作為獲取鎖的標準霞赫,那么一個state變量如何區(qū)分讀鎖和寫鎖呢?答案是通過位運算肥矢,高16位表示讀鎖端衰,低16位表示寫鎖。如果對位運算不太熟悉或者不了解的同學可以看看這篇文章《位運算》甘改。既然是分析讀寫鎖旅东,那么我們先從讀鎖和寫鎖的源碼獲取入手分析。
這里先提前補充一個概念:
寫鎖和讀鎖是互斥的(這里的互斥是指線程間的互斥十艾,當前線程可以獲取到寫鎖又獲取到讀鎖抵代,但是獲取到了讀鎖不能繼續(xù)獲取寫鎖),這是因為讀寫鎖要保持寫操作的可見性忘嫉,如果允許讀鎖在被獲取的情況下對寫鎖的獲取荤牍,那么正在運行的其他讀線程無法感知到當前寫線程的操作。因此庆冕,只有等待其他線程都釋放了讀鎖康吵,寫鎖才能被當前線程獲取,而一旦寫鎖被獲取访递,其他讀寫線程的后續(xù)訪問都會被阻塞晦嵌。
- 寫鎖tryLock()
我們根據(jù)內(nèi)部類WriteLock的調(diào)用關(guān)系找到源碼如下,發(fā)現(xiàn)最終寫鎖調(diào)用的是tryWriteLock()
(以非阻塞獲取鎖方法為例)
public boolean tryLock( ) {
return sync.tryWriteLock();
}
final boolean tryWriteLock() {
Thread current = Thread.currentThread();
int c = getState();
if (c != 0) {//狀態(tài)不等于0拷姿,說明已經(jīng)鎖已經(jīng)被獲取過了
int w = exclusiveCount(c);//這里是判斷是否獲取到了寫鎖惭载,后面會詳細分析這段代碼
// 這里就是判斷是否是鎖重入:2種情況
// 1.c!=0說明是有鎖被獲取的,那么w==0跌前,
// 說明寫鎖是沒有被獲取棕兼,也就是說讀鎖被獲取了,由于寫鎖和讀鎖的互斥抵乓,為了保證數(shù)據(jù)的可見性
// 所以return false.
//2. w!=0伴挚,寫鎖被獲取了靶衍,但是current != getExclusiveOwnerThread() ,
// 說明是被別的線程獲取了茎芋,return false;
if (w == 0 || current != getExclusiveOwnerThread())
return false;
if (w == MAX_COUNT)//判斷是否溢出
throw new Error("Maximum lock count exceeded");
}
// 嘗試獲取鎖
if (!compareAndSetState(c, c + 1))
return false;
setExclusiveOwnerThread(current);
return true;
}
-
讀鎖tryLock()
同樣我們先分析非阻塞獲取鎖方法颅眶,tryReadLock()
final boolean tryReadLock() {
Thread current = Thread.currentThread();
for (;;) {
int c = getState();
if (exclusiveCount(c) != 0 &&
getExclusiveOwnerThread() != current)
return false; //寫鎖被其他線程獲取了,直接返回false
int r = sharedCount(c); //獲取讀鎖的狀態(tài)
if (r == MAX_COUNT)
throw new Error("Maximum lock count exceeded");
if (compareAndSetState(c, c + SHARED_UNIT)) { //嘗試獲取讀鎖
if (r == 0) { //說明第一個獲取到了讀鎖
firstReader = current; //標記下當前線程是第一個獲取的
firstReaderHoldCount = 1; //重入次數(shù)
} else if (firstReader == current) {
firstReaderHoldCount++; //次數(shù)+1
} else {
//cachedHoldCounter 為緩存最后一個獲取鎖的線程
HoldCounter rh = cachedHoldCounter;
if (rh == null || rh.tid != getThreadId(current))
cachedHoldCounter = rh = readHolds.get(); //緩存最后一個獲取鎖的線程
else if (rh.count == 0)// 當前線程獲取到了鎖田弥,但是重入次數(shù)為0涛酗,那么把當前線程存入進去
readHolds.set(rh);
rh.count++;
}
return true;
}
}
}
- 讀鎖的釋放tryReleaseShared()
寫鎖的釋放比較簡單,基本邏輯和讀鎖的釋放是一樣的偷厦,考慮到篇幅商叹,這次主要分析讀鎖的釋放過程:
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(); //釋放完畢只泼,那么久把保存的記錄次數(shù)remove掉
if (count <= 0)
throw unmatchedUnlockException();
}
--rh.count;
}
for (;;) {
int c = getState();
// nextc 是 state 高 16 位減 1 后的值
int nextc = c - SHARED_UNIT;
if (compareAndSetState(c, nextc)) //CAS設(shè)置狀態(tài)
return nextc == 0; //這個判斷如果高 16 位減 1 后的值==0剖笙,那么就是讀狀態(tài)和寫狀態(tài)都釋放了
}
}
上面就是讀寫鎖的獲取和釋放過程源碼,先分析簡單的非阻塞獲取鎖方法请唱,根據(jù)源碼我們可以知道弥咪,寫鎖和讀鎖的是否獲取也是判斷狀態(tài)是否不為0,寫鎖的狀態(tài)獲取方法是exclusiveCount(c)
,讀鎖的狀態(tài)獲取方法是sharedCount(c)
十绑。那么我們接下來分析下這兩個方法是如何對統(tǒng)一個變量位運算獲取各自的狀態(tài)的聚至,在分析之前我們先小結(jié)下前面的內(nèi)容。
- 小結(jié)一下
a. 讀寫鎖依托于AQS的State變量的位運算來區(qū)分讀鎖和寫鎖本橙,高16位表示讀鎖扳躬,低16位表示寫鎖。
b. 為了保證線程間內(nèi)容的可見性甚亭,讀鎖和寫鎖是互斥的坦报,這里的互斥是指線程間的互斥,當前線程可以獲取到寫鎖又獲取到讀鎖狂鞋,但是獲取到了讀鎖不能繼續(xù)獲取寫鎖。
三潜的、Sync 同步器位運算分析
- 狀態(tài)變量按照位劃分示意圖
我們再看看位運算的相關(guān)代碼(我假設(shè)你已經(jīng)知道了位運算的相關(guān)基本知識骚揍,如果不具備,請閱讀《位運算》)
static final int SHARED_SHIFT = 16;
//實際是65536
static final int SHARED_UNIT = (1 << SHARED_SHIFT);
//最大值 65535
static final int MAX_COUNT = (1 << SHARED_SHIFT) - 1;
// 同樣是65535
static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1;
/** 獲取讀的狀態(tài) */
static int sharedCount(int c) { return c >>> SHARED_SHIFT; }
/** 獲取寫鎖的獲取狀態(tài) */
static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; }
我們按照圖示內(nèi)容的數(shù)據(jù)進行運算啰挪,圖示的32位二進制數(shù)據(jù)為:
00000000000000100000000000000011
- 讀狀態(tài)獲取
00000000000000100000000000000011 >>> 16
,無符號右移16位信不,結(jié)果如下:
00000000000000000000000000000010
,換算成10進制數(shù)等于2,說明讀狀態(tài)為: 2
- 讀狀態(tài)獲取
00000000000000100000000000000011 & 65535
亡呵,轉(zhuǎn)換成2進制運算為
00000000000000100000000000000011 & 00000000000000001111111111111111
最后與運算結(jié)果為:
00000000000000100000000000000011
抽活,換算成10進制為3
不得不佩服作者的思想,這種設(shè)計在不修改AQS的代碼前提下锰什,僅僅通過原來的State變量就滿足了讀鎖和寫鎖的分離下硕。
四丁逝、鎖降級
鎖降級是指寫鎖降級為讀鎖。如果當前線程擁有寫鎖梭姓,然后將其釋放霜幼,最后再獲取讀鎖,這種分段完成的過程不能稱之為鎖降級誉尖。鎖降級是指把持鬃锛取(之前擁有的寫鎖的過程)源碼示例(來自于《java并發(fā)編程的藝術(shù)》):
public void processData(){
readLock.lock();
if(!update){
//必須先釋放讀鎖
readLock.unlock();
//鎖降級從寫鎖獲取到開始
writeLock.lock();
try{
if(!update){
update =true;
}
readlock.lock();
}finally{
writeLock.unlock();
}//鎖降級完成,寫鎖降級為讀鎖
}
try{
//略
}finally{
readLock.unlock();
}
}
上述示例就是一個鎖降級的過程铡恕,需要注意的是update變量是一個volatie修飾的變量琢感,所以,線程之間是可見的探熔。該代碼就是獲取到寫鎖后修改變量驹针,然后獲取讀鎖,獲取成功后釋放寫鎖祭刚,完成了鎖的降級牌捷。注意:ReentrantReadWriteLock不支持鎖升級,這是因為如果多個線程獲取到了讀鎖涡驮,其中任何一個線程獲取到了寫鎖暗甥,修改了數(shù)據(jù),其他的線程感知不到數(shù)據(jù)的更新捉捅,這樣就無法保證數(shù)據(jù)的可見性撤防。
最后總結(jié)
- 源碼中,涉及了其他部分棒口,本文做了精簡寄月,比如:
cachedHoldCounter
,firstReader
firstReaderHoldCount
等屬性,這些屬性并沒有對理解原理有多少影響无牵,主要是提升性能的作用漾肮,所以本文沒有討論。 - 讀寫鎖還是依賴于AQS的自定義同步器來實現(xiàn)的茎毁,里面的大部分代碼和之前分析的兩篇文章《Java鎖之ReentrantLock》差不多克懊,AQS的大部分解析已經(jīng)在這兩篇文章已經(jīng)解析過了,如果讀者對此還有疑惑的地方七蜘,可以看看這兩篇文章谭溉。
- 讀寫鎖的巧妙設(shè)計,就是對AQS的鎖狀態(tài)進行為運算橡卤,區(qū)分了讀狀態(tài)和寫狀態(tài)扮念。