前言
線程并發(fā)系列文章:
Java 線程基礎(chǔ)
Java 線程狀態(tài)
Java “優(yōu)雅”地中斷線程-實(shí)踐篇
Java “優(yōu)雅”地中斷線程-原理篇
真正理解Java Volatile的妙用
Java ThreadLocal你之前了解的可能有誤
Java Unsafe/CAS/LockSupport 應(yīng)用與原理
Java 并發(fā)"鎖"的本質(zhì)(一步步實(shí)現(xiàn)鎖)
Java Synchronized實(shí)現(xiàn)互斥之應(yīng)用與源碼初探
Java 對象頭分析與使用(Synchronized相關(guān))
Java Synchronized 偏向鎖/輕量級鎖/重量級鎖的演變過程
Java Synchronized 重量級鎖原理深入剖析上(互斥篇)
Java Synchronized 重量級鎖原理深入剖析下(同步篇)
Java并發(fā)之 AQS 深入解析(上)
Java并發(fā)之 AQS 深入解析(下)
Java Thread.sleep/Thread.join/Thread.yield/Object.wait/Condition.await 詳解
Java 并發(fā)之 ReentrantLock 深入分析(與Synchronized區(qū)別)
Java 并發(fā)之 ReentrantReadWriteLock 深入分析
Java Semaphore/CountDownLatch/CyclicBarrier 深入解析(原理篇)
Java Semaphore/CountDownLatch/CyclicBarrier 深入解析(應(yīng)用篇)
最詳細(xì)的圖文解析Java各種鎖(終極篇)
線程池必懂系列
上篇文章分析了AQS的實(shí)際應(yīng)用之一:ReentrantLock 的實(shí)現(xiàn)。ReentrantLock 和synchronized 都是獨(dú)占鎖楼雹,而AQS還支持共享鎖贮缅,本篇就來分析AQS 共享鎖的實(shí)際應(yīng)用。
通過本篇文章齿坷,你將了解到:
1崎场、共享鎖照雁、獨(dú)享鎖區(qū)別
2、讀鎖的實(shí)現(xiàn)原理
3悬嗓、寫鎖的實(shí)現(xiàn)原理
4包竹、讀寫鎖 tryLock 原理
5周瞎、讀寫鎖的應(yīng)用
1声诸、共享鎖彼乌、獨(dú)享鎖區(qū)別
基本差別
共享鎖慰照、獨(dú)占鎖是在AQS里實(shí)現(xiàn)的,核心是"state"的值:
如上圖,對于共享鎖來說榛泛,允許多個(gè)線程對state進(jìn)行有效修改噩斟。
讀寫鎖的引入
根據(jù)上面的圖剃允,state 同時(shí)只能表示一種鎖椒楣,要么獨(dú)占鎖牡肉,要么共享鎖统锤。而在實(shí)際的應(yīng)用場景里經(jīng)常會(huì)碰到多個(gè)線程讀煌寇,多個(gè)線程寫的情況逾雄,此時(shí)為了能夠協(xié)同讀、寫線程永品,需要將state改造鼎姐。
先來看AQS state 定義:
#AbstractQueuedSynchronizer.java
private volatile int state;
可以看出是int 類型的(當(dāng)然也有l(wèi)ong 類型的症见,在AbstractQueuedLongSynchronizer.java 里谋作,本文以int 為例)
state 被分為兩部分遵蚜,低16位表示寫鎖(獨(dú)占鎖)吭净,高16位表示讀鎖(共享鎖),這樣一個(gè)32位的state 就可以同時(shí)表示共享鎖和獨(dú)占鎖了友扰。
2庶柿、讀鎖的實(shí)現(xiàn)原理
ReentrantReadWriteLock 的構(gòu)造
ReentrantReadWriteLock 并沒有像ReentrantLock一樣直接實(shí)現(xiàn)Lock 接口甚负,而是內(nèi)部分別持有ReadLock审残、WriteLock類型的成員變量碰辅,兩者均實(shí)現(xiàn)了Lock 接口介时。
#ReentrantReadWriteLock.java
public ReentrantReadWriteLock() {
//默認(rèn)非公平鎖
this(false);
}
public ReentrantReadWriteLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
//構(gòu)造讀鎖
readerLock = new ReadLock(this);
//構(gòu)造寫鎖
writerLock = new WriteLock(this);
}
ReentrantReadWriteLock 默認(rèn)實(shí)現(xiàn)非公平鎖循衰,讀鎖会钝、寫鎖支持非公平鎖和公平鎖迁酸。
讀寫鎖構(gòu)造之后奸鬓,將鎖暴露出來給外部使用:
#ReentrantReadWriteLock.java
//獲取寫鎖對象
public ReentrantReadWriteLock.WriteLock writeLock() { return writerLock; }
//獲取讀鎖對象
public ReentrantReadWriteLock.ReadLock readLock() { return readerLock; }
獲取鎖
在ReentrantLock 分析獨(dú)占鎖時(shí)有如下圖:
與獨(dú)占鎖類似,AQS雖然已經(jīng)實(shí)現(xiàn)了共享鎖的基本邏輯澡罚,但是真正獲取鎖留搔、釋放鎖的操作還是需要子類實(shí)現(xiàn)隔显,共享鎖需要實(shí)現(xiàn)方法:
tryAcquireShared & tryReleaseShared
來看看獲取鎖的過程:
#ReentrantReadWriteLock.ReadLock
public void lock() {
//共享鎖
sync.acquireShared(1);
}
#AbstractQueuedSynchronizer.java
public final void acquireShared(int arg) {
if (tryAcquireShared(arg) < 0)
//doAcquireShared 在AQS里實(shí)現(xiàn)
doAcquireShared(arg);
}
重點(diǎn)是tryAcquireShared(xx):
#ReentrantReadWriteLock.java
protected final int tryAcquireShared(int unused) {
Thread current = Thread.currentThread();
//獲取同步狀態(tài)
int c = getState();
//此處exclusiveCount作用是取state 低16位,若是不等于0梳毙,說明有線程占有了寫鎖
//若是有線程占有了寫鎖萌业,而這個(gè)線程不是當(dāng)前線程生年,則直接退出------------>(1)
if (exclusiveCount(c) != 0 &&
getExclusiveOwnerThread() != current)
return -1;
//獲取state 高16位抱婉,若是大于0蒸绩,說明有線程占有了讀鎖
int r = sharedCount(c);
//當(dāng)前線程是否應(yīng)該阻塞
if (!readerShouldBlock() &&//------------>(2)
r < MAX_COUNT &&//若是不該阻塞传蹈,則嘗試CAS修改state高16位的值
compareAndSetState(c, c + SHARED_UNIT)) {
//--------記錄線程/重入次數(shù)----------->(3)
//修改state 成功步藕,說明成功占有了讀鎖
if (r == 0) {
//記錄第一個(gè)占有讀鎖的線程
firstReader = current;
//占有次數(shù)為1
firstReaderHoldCount = 1;
} else if (firstReader == current) {
//第一個(gè)占有讀鎖的線程重入了該鎖
firstReaderHoldCount++;
} else {
//是其它線程占有鎖
//取出緩存的HoldCounter
HoldCounter rh = cachedHoldCounter;
//若是緩存為空沾歪,或是緩存存儲(chǔ)的不是當(dāng)前的線程
if (rh == null || rh.tid != getThreadId(current))
//從threadLocal里獲取
//readHolds 為ThreadLocalHoldCounter 類型,繼承自ThreadLocal
cachedHoldCounter = rh = readHolds.get();
else if (rh.count == 0)
//說明cachedHoldCounter 已經(jīng)被移出threadLocal仪或,
//重新加入即可------------>(4)
readHolds.set(rh);
//記錄重入次數(shù)
rh.count++;
//--------記錄線程/重入次數(shù)-----------
}
return 1;
}
//------------>(5)
return fullTryAcquireShared(current);
}
以上是獲取讀鎖的核心代碼蕾域,標(biāo)注了5個(gè)重點(diǎn)旨巷,分別來分析。
(1)
此處表明了一個(gè)信息:
若是當(dāng)前線程已經(jīng)獲取了寫鎖斧吐,那么它可以繼續(xù)嘗試獲得讀鎖煤率。
當(dāng)它把寫鎖釋放后蝶糯,只剩讀鎖了识虚。這個(gè)過程可以理解為鎖的降級舷礼。
(2)
線程能否有機(jī)會(huì)獲取讀鎖,還需要經(jīng)過兩個(gè)判斷:
1团赁、判定readerShouldBlock()。
2怀挠、判定讀鎖個(gè)數(shù)用完了沒,閾值是2^16-1害捕。
而讀鎖公平與否就體現(xiàn)在readerShouldBlock()的實(shí)現(xiàn)上绿淋。
先來看非公平讀鎖:
#ReentrantReadWriteLock.java
final boolean readerShouldBlock() {
return apparentlyFirstQueuedIsExclusive();
}
#AbstractQueuedSynchronizer.java
final boolean apparentlyFirstQueuedIsExclusive() {
//判斷等待隊(duì)列里的第二個(gè)節(jié)點(diǎn)是否在等待寫鎖
Node h, s;
return (h = head) != null &&
(s = h.next) != null &&
!s.isShared() &&
s.thread != null;
}
若等待隊(duì)列里的第二個(gè)節(jié)點(diǎn)是在等待寫鎖,那么此時(shí)不能去獲取讀鎖尝盼。
這與ReentrantLock不一樣吞滞,ReentrantLock 非公平鎖的實(shí)現(xiàn)是不管等待隊(duì)列里有沒有節(jié)點(diǎn),都會(huì)去嘗試獲取鎖盾沫。
再來看公平讀鎖
#ReentrantReadWriteLock.java
final boolean readerShouldBlock() {
return hasQueuedPredecessors();
}
判斷隊(duì)列里是否有更早于當(dāng)前線程排隊(duì)的節(jié)點(diǎn)裁赠,該方法在上篇分析ReentrantLock 時(shí)有深入分析,此處不再贅述。
(3)
這部分代碼看起來多,實(shí)際上就是為了記錄重入次數(shù)以及為了效率考慮引入了一些緩存掀潮。
考慮到有可能始終只有一個(gè)線程獲取讀鎖薯鼠,因此定義了兩個(gè)變量還記錄重入次數(shù):
#ReentrantReadWriteLock.java
//記錄第一個(gè)獲取讀鎖的線程
private transient Thread firstReader = null;
//第一個(gè)獲取讀鎖的線程獲取讀鎖的個(gè)數(shù)
private transient int firstReaderHoldCount;
再考慮到有多個(gè)線程獲取鎖,它們也需要記錄獲取鎖的個(gè)數(shù),與線程綁定的數(shù)據(jù)我們想到了ThreadLocal,于是定義了:
private transient ThreadLocalHoldCounter readHolds;
來記錄HoldCounter(存儲(chǔ)獲取鎖的個(gè)數(shù)及綁定的線程id)。
最后為了不用每次都去ThreadLocal里查詢數(shù)據(jù),再定義了變量來緩存HoldCounter:
#ReentrantReadWriteLock.java
private transient HoldCounter cachedHoldCounter;
(4)
cachedHoldCounter.count == 0,是在tryReleaseShared(xx)里操作的,并且判斷當(dāng)線程已經(jīng)徹底釋放了讀鎖后簿透,將HoldCounter 從ThreadLocal里移除啡浊,因此此處需要加回來廷粒。
(5)
走到這一步,說明之前獲取鎖的操作失敗了,原因有三點(diǎn):
1、readerShouldBlock() == true。
2、r >= MAX_COUNT。
3、中途有其它線程修改了state。
fullTryAcquireShared(xx)與tryAcquireShared(xx)很類似,目的就是為了獲取鎖。
針對第三點(diǎn),fullTryAcquireShared(xx)里有個(gè)死循環(huán),不斷獲取state值募判,若是符合1、2點(diǎn)浸船,則退出循環(huán),否則嘗試CAS修改state阔籽,若是失敗,則繼續(xù)循環(huán)獲取state值。
小結(jié)一下:
1、fullTryAcquireShared(xx) 獲取鎖失敗返回-1横漏,接下來的處理邏輯流轉(zhuǎn)到AQS里二蓝,線程可能會(huì)被掛起鸥诽。
2炬藤、fullTryAcquireShared(xx) 獲取鎖成功則返回1帝火。
釋放鎖
釋放鎖的邏輯比較簡單:
#ReentrantReadWriteLock.ReadLock
public void lock() {
sync.acquireShared(1);
}
#AbstractQueuedSynchronizer.java
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
//在AQS里實(shí)現(xiàn)
doReleaseShared();
return true;
}
return false;
}
重點(diǎn)是tryReleaseShared(xx):
#ReentrantReadWriteLock.java
protected final boolean tryReleaseShared(int unused) {
Thread current = Thread.currentThread();
//當(dāng)前線程是之前第一個(gè)獲取讀鎖的線程
if (firstReader == current) {
if (firstReaderHoldCount == 1)
//徹底釋放完了冕广,置空
firstReader = null;
else
firstReaderHoldCount--;
} else {
//先從緩存里取
HoldCounter rh = cachedHoldCounter;
if (rh == null || rh.tid != getThreadId(current))
//取不到,則需要從ThreadLocal里取
rh = readHolds.get();
int count = rh.count;
if (count <= 1) {
//若是當(dāng)前線程不再占有鎖隘谣,則清除對應(yīng)的ThreadLocal變量
readHolds.remove();
if (count <= 0)
throw unmatchedUnlockException();
}
--rh.count;
}
for (;;) {
//修改state
int c = getState();
int nextc = c - SHARED_UNIT;
if (compareAndSetState(c, nextc))
//若是state值變?yōu)?弟晚,說明讀鎖、寫鎖都釋放完了
return nextc == 0;
}
}
此處需要注意的是:
tryReleaseShared(xx)釋放讀鎖時(shí)候怀偷,若是沒有完全釋放讀鎖脓斩、寫鎖照皆,那么將會(huì)返回false。
而在AQS里釋放共享鎖流程如下:
#AbstractQueuedSynchronizer.java
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
doReleaseShared();
return true;
}
return false;
}
也就是說此種情況下败玉,doReleaseShared() 將不會(huì)被調(diào)用财剖,也就不會(huì)喚醒同步隊(duì)列里的節(jié)點(diǎn)匣摘。
這么做的原因是:
若只釋放完讀鎖,還剩寫鎖被占用贯被。而因?yàn)閷戞i是獨(dú)占鎖朱沃,其它線程無法獲取鎖失暴,那么即使喚醒了它們也沒有用。
3喳逛、寫鎖的實(shí)現(xiàn)原理
獲取鎖
寫鎖是獨(dú)占鎖赠法,因此重點(diǎn)關(guān)注tryAcquire(xx):
#ReentrantReadWriteLock.java
protected final boolean tryAcquire(int acquires) {
Thread current = Thread.currentThread();
//獲取同步狀態(tài)
int c = getState();
//獲取當(dāng)前寫鎖個(gè)數(shù)
int w = exclusiveCount(c);
if (c != 0) {
//1、若是w==0窟坐,而c!= 0懒豹,說明有線程占有了讀鎖片酝,不能再獲取寫鎖了
//2、若是寫鎖被占用吊圾,但是不是當(dāng)前線程,則不能再獲取寫鎖了
if (w == 0 || current != getExclusiveOwnerThread())
return false;
//鎖個(gè)數(shù)超限了
if (w + exclusiveCount(acquires) > MAX_COUNT)
throw new Error("Maximum lock count exceeded");
//走到此處,說明重入仆嗦,直接設(shè)置罕袋,同一時(shí)刻只有一個(gè)線程能走到這
setState(c + acquires);
return true;
}
//若c==0唠摹,此時(shí)讀鎖盗温、寫鎖都沒線程占用
//判斷線程是否應(yīng)該被阻塞染坯,否則嘗試獲取寫鎖------->(1)
if (writerShouldBlock() ||
!compareAndSetState(c, c + acquires))
return false;
//獨(dú)占鎖需要關(guān)聯(lián)線程
setExclusiveOwnerThread(current);
return true;
}
來看看writerShouldBlock()骡技,寫鎖公平/非公平就在此處實(shí)現(xiàn)的唆途。
先來看非公平寫鎖:
#ReentrantReadWriteLock.java
final boolean writerShouldBlock() {
//不阻塞
return false; // writers can always barge
}
非公平寫鎖不應(yīng)該阻塞。
再來看公平寫鎖:
#ReentrantReadWriteLock.java
final boolean writerShouldBlock() {
//判斷隊(duì)列是否有有效節(jié)點(diǎn)等待
return hasQueuedPredecessors();
}
和公平讀鎖一樣的判斷條件。
小結(jié)
1、讀鎖/寫鎖 已被其它線程占用医舆,那么新來的線程將無法獲取寫鎖俘侠。
2、寫鎖可重入蔬将。
釋放鎖
釋放鎖重點(diǎn)關(guān)注tryRelease(xx):
##ReentrantReadWriteLock.java
protected final boolean tryRelease(int releases) {
//當(dāng)前線程是否持有寫鎖
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
//同一時(shí)刻爷速,只有一個(gè)線程會(huì)執(zhí)行到此
int nextc = getState() - releases;
//判斷寫鎖是否釋放完畢
boolean free = exclusiveCount(nextc) == 0;
if (free)
//取消關(guān)聯(lián)
setExclusiveOwnerThread(null);
//設(shè)置狀態(tài)
setState(nextc);
return free;
}
若tryRelease(xx)返回true,則AQS里會(huì)喚醒等待隊(duì)列的線程霞怀。
4惫东、讀寫鎖 tryLock 原理
讀鎖tryLock
#ReentrantReadWriteLock.java
public boolean tryLock() {
return sync.tryReadLock();
}
final boolean tryReadLock() {
Thread current = Thread.currentThread();
for (;;) {
//for 循環(huán)為了檢測最新的state
int c = getState();
if (exclusiveCount(c) != 0 &&
getExclusiveOwnerThread() != current)
return false;
int r = sharedCount(c);
if (r == MAX_COUNT)
throw new Error("Maximum lock count exceeded");
if (compareAndSetState(c, c + SHARED_UNIT)) {
//記錄次數(shù)
if (r == 0) {
firstReader = current;
firstReaderHoldCount = 1;
} else if (firstReader == current) {
firstReaderHoldCount++;
} else {
HoldCounter rh = cachedHoldCounter;
if (rh == null || rh.tid != getThreadId(current))
cachedHoldCounter = rh = readHolds.get();
else if (rh.count == 0)
readHolds.set(rh);
rh.count++;
}
//獲得鎖后退出循環(huán)
return true;
}
}
}
可以看出tryReadLock(xx)里: 只要不是別的線程占有寫鎖并且讀鎖個(gè)數(shù)沒超出限制,那么它將一直嘗試獲取讀鎖,直到得到為止廉沮。
寫鎖tryLock
public boolean tryLock() {
return sync.tryWriteLock();
}
final boolean tryWriteLock() {
Thread current = Thread.currentThread();
int c = getState();
if (c != 0) {
int w = exclusiveCount(c);
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;
}
寫鎖只嘗試一次CAS颓遏,失敗就返回。
最終滞时,用圖表示讀鎖叁幢、寫鎖實(shí)現(xiàn)的功能:
讀鎖與寫鎖關(guān)系:
5、讀寫鎖的應(yīng)用
分析完原理坪稽,來看看簡單應(yīng)用曼玩。
public class TestThread {
static ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
static ReentrantReadWriteLock.ReadLock readLock = readWriteLock.readLock();
static ReentrantReadWriteLock.WriteLock writeLock = readWriteLock.writeLock();
public static void main(String args[]) {
//讀
for (int i = 0; i < 10; i++) {
new Thread(new Runnable() {
@Override
public void run() {
String threadName = Thread.currentThread().getName();
try {
System.out.println("thread " + threadName + " acquire read lock");
readLock.lock();
System.out.println("thread " + threadName + " read locking");
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
readLock.unlock();
System.out.println("thread " + threadName + " release read lock remain read count:" + readWriteLock.getReadLockCount());
}
}
}, "" + i).start();
}
//寫
for (int i = 0; i < 10; i++) {
new Thread(new Runnable() {
@Override
public void run() {
String threadName = Thread.currentThread().getName();
try {
System.out.println("thread " + threadName + " acquire write lock");
writeLock.lock();
System.out.println("thread " + threadName + " write locking");
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
writeLock.unlock();
System.out.println("thread " + threadName + " release write lock remain write count:" + readWriteLock.getWriteHoldCount());
}
}
}, "" + i).start();
}
}
}
10個(gè)線程獲取讀鎖,10個(gè)線程獲取寫鎖窒百。
讀寫鎖應(yīng)用場景:
- ReentrantReadWriteLock 適用于讀多寫少的場景黍判,提高多線程讀的效率、吞吐量篙梢。
同一線程讀鎖顷帖、寫鎖關(guān)系:
public class TestThread {
static ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
static ReentrantReadWriteLock.ReadLock readLock = readWriteLock.readLock();
static ReentrantReadWriteLock.WriteLock writeLock = readWriteLock.writeLock();
public static void main(String args[]) {
// new TestThread().testReadWriteLock();------>1、先讀鎖庭猩,后寫鎖
// new TestThread().testWriteReadLock();------>2窟她、先寫鎖、后讀鎖
}
private void testReadWriteLock() {
System.out.println("before read lock");
readLock.lock();
System.out.println("before write lock");
writeLock.lock();
System.out.println("after write lock");
}
private void testWriteReadLock() {
System.out.println("before write lock");
writeLock.lock();
System.out.println("before read lock");
readLock.lock();
System.out.println("after read lock");
}
}
分別打開1蔼水、2 注釋震糖,發(fā)現(xiàn):
1、先獲取讀鎖趴腋,再獲取寫鎖吊说,則線程在寫鎖處掛起。
2优炬、先獲取寫鎖颁井,再獲取讀鎖,則都能正常獲取鎖蠢护。
這與我們上述的理論分析一致雅宾。
下篇將會(huì)分析Semaphore、CountDownLatch葵硕、 CyclicBarrier原理及其應(yīng)用眉抬。
本文基于jdk1.8。