文章來源公眾號(hào)三不猴子
J.U.C 簡介
Java.util.concurrent 是在并發(fā)編程中比較常用的工具類惰匙,里面包含很多用來在并發(fā)場景中使用的組件技掏。比如線程池、阻塞隊(duì)列项鬼、計(jì)時(shí)器哑梳、同步器、并發(fā)集合等等绘盟。并發(fā)包的作者是大名鼎鼎的 Doug Lea鸠真。我們在接下來,回去剖析一些經(jīng)典的比較常用的組件的設(shè)計(jì)思想
Lock
Lock 在 J.U.C 中是最核心的組件龄毡,前面我們講 synchronized 的時(shí)候說過吠卷,鎖最重要的特性就是解決并發(fā)安全問題。為什么要以 Lock 作為切入點(diǎn)呢沦零?如果有同學(xué)看過 J.U.C 包中的所有組件祭隔,一定會(huì)發(fā)現(xiàn)絕大部分的組件都有用到了 Lock。所以通過 Lock 作為切入點(diǎn)使得在后續(xù)的學(xué)習(xí)過程中會(huì)更加輕松路操。
Lock 簡介
在 Lock 接口出現(xiàn)之前疾渴,Java 中的應(yīng)用程序?qū)τ诙嗑€程的并發(fā)安全處理只能基于synchronized 關(guān)鍵字來解決。但是 synchronized 在有些場景中會(huì)存在一些短板屯仗,也就是它并不適合于所有的并發(fā)場景搞坝。但是在 Java5 以后,Lock 的出現(xiàn)可以解決synchronized 在某些場景中的短板魁袜,它比 synchronized 更加靈活桩撮。
Lock 的實(shí)現(xiàn)
Lock 本質(zhì)上是一個(gè)接口,它定義了釋放鎖和獲得鎖的抽象方法峰弹,定義成接口就意味著它定義了鎖的一個(gè)標(biāo)準(zhǔn)規(guī)范距境,也同時(shí)意味著鎖的不同實(shí)現(xiàn)。實(shí)現(xiàn) Lock 接口的類有很多垮卓,以下為幾個(gè)常見的鎖實(shí)現(xiàn)
- ReentrantLock:表示重入鎖,它是唯一一個(gè)實(shí)現(xiàn)了 Lock 接口的類师幕。重入鎖指的是線程在獲得鎖之后粟按,再次獲取該鎖不需要阻塞诬滩,而是直接關(guān)聯(lián)一次計(jì)數(shù)器增加重入次數(shù)
- ReentrantReadWriteLock:重入讀寫鎖,它實(shí)現(xiàn)了 ReadWriteLock 接口灭将,在這個(gè)類中維護(hù)了兩個(gè)鎖疼鸟,一個(gè)是 ReadLock,一個(gè)是 WriteLock庙曙,他們都分別實(shí)現(xiàn)了 Lock接口空镜。讀寫鎖是一種適合讀多寫少的場景下解決線程安全問題的工具,基本原則是: 讀和讀不互斥捌朴、讀和寫互斥吴攒、寫和寫互斥。也就是說涉及到影響數(shù)據(jù)變化的操作都會(huì)存在互斥砂蔽。
- StampedLock: stampedLock 是 JDK8 引入的新的鎖機(jī)制洼怔,可以簡單認(rèn)為是讀寫鎖的一個(gè)改進(jìn)版本,讀寫鎖雖然通過分離讀和寫的功能使得讀和讀之間可以完全并發(fā)左驾,但是讀和寫是有沖突的镣隶,如果大量的讀線程存在,可能會(huì)引起寫線程的饑餓诡右。stampedLock 是一種樂觀的讀策略安岂,使得樂觀鎖完全不會(huì)阻塞寫線程
Lock 的類關(guān)系圖
Lock 有很多的鎖的實(shí)現(xiàn),但是直觀的實(shí)現(xiàn)是 ReentrantLock 重入鎖
void lock() // 如果鎖可用就獲得鎖帆吻,如果鎖不可用就阻塞直到鎖釋放
void lockInterruptibly() // 和lock()方法相似, 但阻塞的線程 可 中 斷 域那, 拋 出
java.lang.InterruptedException 異常
boolean tryLock() // 非阻塞獲取鎖;嘗試獲取鎖,如果成功返回 true
boolean tryLock(long timeout, TimeUnit timeUnit) //帶有超時(shí)時(shí)間的獲取鎖方法
void unlock() // 釋放鎖
ReentrantLock 重入鎖
重入鎖桅锄,表示支持重新進(jìn)入的鎖琉雳,也就是說,如果當(dāng)前線程 t1 通過調(diào)用 lock 方法獲取了鎖之后友瘤,再次調(diào)用 lock翠肘,是不會(huì)再阻塞去獲取鎖的,直接增加重試次數(shù)就行了辫秧。synchronized 和 ReentrantLock 都是可重入鎖束倍。很多同學(xué)不理解為什么鎖會(huì)存在重入的特性,那是因?yàn)閷τ谕芥i的理解程度還不夠盟戏,比如在存在多個(gè)加鎖的方法的相互調(diào)用绪妹,其實(shí)就是一種重入特性的場景。
重入鎖的設(shè)計(jì)目的
比如調(diào)用 demo 方法獲得了當(dāng)前的對象鎖柿究,然后在這個(gè)方法中再去調(diào)用demo2邮旷,demo2 中的存在同一個(gè)實(shí)例鎖,這個(gè)時(shí)候當(dāng)前線程會(huì)因?yàn)闊o法獲得demo2 的對象鎖而阻塞蝇摸,就會(huì)產(chǎn)生死鎖婶肩。重入鎖的設(shè)計(jì)目的是避免線程的死鎖办陷。
public class ReentrantDemo {
public synchronized void demo() {
System.out.println("begin:demo");
demo2();
}
}
public void demo2() {
System.out.println("begin:demo1");
synchronized (this) {
}
public static void main(String[] args) {
ReentrantDemo rd = new ReentrantDemo();
new Thread(rd::demo).start();
}
public class AtomicDemo {
private static int count = 0;
static Lock lock = new ReentrantLock();
public static void inc() {
lock.lock();
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
count++;
lock.unlock();
}
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 1000; i++) {
new Thread(() -> {
AtomicDemo.inc();
}).start();
}
Thread.sleep(3000);
System.out.println("result:" + count);
}
}
ReentrantReadWriteLock
我們以前理解的鎖,基本都是排他鎖律歼,也就是這些鎖在同一時(shí)刻只允許一個(gè)線程進(jìn)行訪問民镜,而讀寫所在同一時(shí)刻可以允許多個(gè)線程訪問,但是在寫線程訪問時(shí)险毁,所有的讀線程和其他寫線程都會(huì)被阻塞制圈。讀寫鎖維護(hù)了一對鎖,一個(gè)讀鎖畔况、一個(gè)寫鎖; 一般情況下鲸鹦,讀寫鎖的性能都會(huì)比排它鎖好,因?yàn)榇蠖鄶?shù)場景讀是多于寫的问窃。在讀多于寫的情況下亥鬓,讀寫鎖能夠提供比排它鎖更好的并發(fā)性和吞吐量.
public class LockDemo {
static Map<String, Object> cacheMap = new HashMap<>();
static ReentrantReadWriteLock rwl = new
ReentrantReadWriteLock();
static Lock read = rwl.readLock();
static Lock write = rwl.writeLock();
public static final Object get(String key) {
System.out.println("開始讀取數(shù)據(jù)");
read.lock(); //讀鎖
try {
return cacheMap.get(key);
} finally {
read.unlock();
}
}
public static final Object put(String key, Object value) {
write.lock();
System.out.println("開始寫數(shù)據(jù)");
try {
return cacheMap.put(key, value);
} finally {
write.unlock();
}
}
}
在這個(gè)案例中,通過 hashmap 來模擬了一個(gè)內(nèi)存緩存域庇,然后使用讀寫所來保證這個(gè)內(nèi)存緩存的線程安全性嵌戈。當(dāng)執(zhí)行讀操作的時(shí)候,需要獲取讀鎖听皿,在并發(fā)訪問的時(shí)候熟呛,讀鎖不會(huì)被阻塞,因?yàn)樽x操作不會(huì)影響執(zhí)行結(jié)果尉姨。在執(zhí)行寫操作是庵朝,線程必須要獲取寫鎖,當(dāng)已經(jīng)有線程持有寫鎖的情況下又厉,當(dāng)前線程會(huì)被阻塞九府,只有當(dāng)寫鎖釋放以后,其他讀寫操作才能繼續(xù)執(zhí)行覆致。使用讀寫鎖提升讀操作的并發(fā)性侄旬,也保證每次寫操作對所有的讀寫操作的可見性
- 讀鎖與讀鎖可以共享
- 讀鎖與寫鎖不可以共享(排他)
- 寫鎖與寫鎖不可以共享(排他
ReentrantLock 的實(shí)現(xiàn)原理
我們知道鎖的基本原理是,基于將多線程并行任務(wù)通過某一種機(jī)制實(shí)現(xiàn)線程的串行執(zhí)行煌妈,從而達(dá)到線程安全性的目的儡羔。在 synchronized 中,我們分析了偏向鎖璧诵、輕量級鎖汰蜘、樂觀鎖≈蓿基于樂觀鎖以及自旋鎖來優(yōu)化了 synchronized 的加鎖開銷族操,同時(shí)在重量級鎖階段,通過線程的阻塞以及喚醒來達(dá)到線程競爭和同步的目的比被。
那么在 ReentrantLock 中坪创,也一定會(huì)存在這樣的需要去解決的問題炕婶。就是在多線程競爭重入鎖時(shí),競爭失敗的線程是如何實(shí)現(xiàn)阻塞以及被喚醒的呢莱预?
AQS 是什么
在 Lock 中,用到了一個(gè)同步隊(duì)列 AQS项滑,全稱 AbstractQueuedSynchronizer依沮,它是一個(gè)同步工具也是 Lock 用來實(shí)現(xiàn)線程同步的核心組件。如果你搞懂了 AQS枪狂,那么 J.U.C 中絕大部分的工具都能輕松掌握危喉。
AQS 的兩種功能
從使用層面來說,AQS 的功能分為兩種:獨(dú)占和共享獨(dú)占鎖州疾,每次只能有一個(gè)線程持有鎖辜限,比如前面給大家演示的 ReentrantLock 就是
以獨(dú)占方式實(shí)現(xiàn)的互斥鎖共 享 鎖 , 允 許 多 個(gè) 線 程 同 時(shí) 獲 取 鎖 严蓖, 并 發(fā) 訪 問 共 享 資 源 薄嫡, 比 如ReentrantReadWriteLock
AQS 的內(nèi)部實(shí)現(xiàn)
首先,AQS維護(hù)了一個(gè)volatile的狀態(tài)變量state颗胡,該變量表示同步器的狀態(tài)維護(hù)基于一個(gè)int值毫深,至于這個(gè)state值的含義,從抽象意義來說它就是同步狀態(tài)(廢話)毒姨。具體來說哑蔫,所有可以基于一個(gè)int值來維護(hù)線程對共享資源的持有狀態(tài)(當(dāng)然也結(jié)合其他的API)的組件都可以基于AQS實(shí)現(xiàn),比如:
對于ReentrantLock中實(shí)現(xiàn)的AQS子類Sync,state表示線程加鎖次數(shù)(0表示未加鎖弧呐,否則表示被同一個(gè)線程加鎖了多少次-可重入性)
對于ReentrantReadWriteLock來說闸迷,state的高16位表示線程對讀鎖的加鎖次數(shù),低16位表示線程對寫鎖的加鎖次數(shù)(當(dāng)然讀鎖俘枫、寫鎖也分別是可重入的腥沽,并且會(huì)有互斥性,這些后面詳細(xì)說明)
對于Semaphore來說崩哩,state表示其可用信號(hào)量巡球,簡短不嚴(yán)謹(jǐn)?shù)恼f法可以是:state的數(shù)值表示其還可以被線程獲取的次數(shù),0表示信號(hào)量已經(jīng)被耗盡(暫不可用)
-
對于CountDownLatch來說邓嘹,state表示鎖閂需要被解鎖的次數(shù)(release)酣栈,可以實(shí)現(xiàn)多個(gè)線程執(zhí)行release動(dòng)作后state變?yōu)?標(biāo)識(shí)該鎖閂被打開(對應(yīng)阻塞的線程此時(shí)才能被釋放執(zhí)行),進(jìn)而通過計(jì)數(shù)器實(shí)現(xiàn)多個(gè)線程前置動(dòng)作全部完成后才能執(zhí)行后續(xù)動(dòng)作的作用
AQS定義了靜態(tài)內(nèi)部類Node(充當(dāng)隊(duì)列元素的角色)汹押,并且持有兩個(gè)Node的引用head矿筝、tail,就是個(gè)常規(guī)的隊(duì)列也沒啥好說的棚贾。Node內(nèi)定義了其
prev(前驅(qū)節(jié)點(diǎn)) next(后繼節(jié)點(diǎn))
thread(與當(dāng)前節(jié)點(diǎn)關(guān)聯(lián)的線程)
waitStatus(當(dāng)前隊(duì)列節(jié)點(diǎn)的等待狀態(tài)窖维,不同的狀態(tài)有不同的含義榆综,可能的狀態(tài)可以上面的一些靜態(tài)變量如SIGNAL等),waitStatus與鎖的搶占以及狀態(tài)同步相關(guān)铸史,后面我們會(huì)結(jié)合源碼詳細(xì)展開
SHARED鼻疮、EXCLUSIVE,簡單的Node實(shí)例琳轿,只是用來表示占有模式(共享或獨(dú)占)
nextWaiter判沟,表示下一個(gè)等待節(jié)點(diǎn)(這個(gè)是和Condition相關(guān)的,對目前的討論沒有影響崭篡,后面會(huì)展開討論)
-
一些變量原子操作變量如NEXT用來原子操作next挪哄,實(shí)現(xiàn)Node內(nèi)變量的CAS原子操作
釋放鎖以及添加線程對于隊(duì)列的變化
當(dāng)出現(xiàn)鎖競爭以及釋放鎖的時(shí)候,AQS 同步隊(duì)列中的節(jié)點(diǎn)會(huì)發(fā)生變化琉闪,首先看一下添加節(jié)點(diǎn)的場景迹炼。這里里會(huì)涉及到兩個(gè)變化
新的線程封裝成 Node 節(jié)點(diǎn)追加到同步隊(duì)列中,設(shè)置 prev 節(jié)點(diǎn)以及修改當(dāng)前節(jié)點(diǎn)的前置節(jié)點(diǎn)的 next 節(jié)點(diǎn)指向自己
-
通過 CAS 講 tail 重新指向新的尾部節(jié)點(diǎn)head 節(jié)點(diǎn)表示獲取鎖成功的節(jié)點(diǎn)颠毙,當(dāng)頭結(jié)點(diǎn)在釋放同步狀態(tài)時(shí)斯入,會(huì)喚醒后繼節(jié)點(diǎn),如果后繼節(jié)點(diǎn)獲得鎖成功吟秩,會(huì)把自己設(shè)置為頭結(jié)點(diǎn)咱扣,節(jié)點(diǎn)的變化過程如下這個(gè)過程也是涉及到兩個(gè)變化
修改 head 節(jié)點(diǎn)指向下一個(gè)獲得鎖的節(jié)點(diǎn)
新的獲得鎖的節(jié)點(diǎn),將 prev 的指針指向 null
設(shè)置 head 節(jié)點(diǎn)不需要用 CAS涵防,原因是設(shè)置 head 節(jié)點(diǎn)是由獲得鎖的線程來完成的闹伪,而同步鎖只能由一個(gè)線程獲得,所以不需要 CAS 保證壮池,只需要把 head 節(jié)點(diǎn)設(shè)置為原首節(jié)點(diǎn)的后繼節(jié)點(diǎn)偏瓤,并且斷開原 head 節(jié)點(diǎn)的 next 引用即可
ReentrantLock 的源碼分析
以 ReentrantLock 作為切入點(diǎn),來看看在這個(gè)場景中是如何使用 AQS 來實(shí)現(xiàn)線程的同步的
ReentrantLock 的時(shí)序圖
調(diào)用 ReentrantLock 中的 lock()方法椰憋,源碼的調(diào)用過程我使用了時(shí)序圖來展現(xiàn)厅克。
ReentrantLock.lock()
這個(gè)是 reentrantLock 獲取鎖的入口
public void lock() {
sync.lock();
}
sync 實(shí)際上是一個(gè)抽象的靜態(tài)內(nèi)部類,它繼承了 AQS 來實(shí)現(xiàn)重入鎖的邏輯橙依,我們前面說過 AQS 是一個(gè)同步隊(duì)列证舟,它能夠?qū)崿F(xiàn)線程的阻塞以及喚醒,但它并不具備業(yè)務(wù)功能窗骑,所以在不同的同步場景中女责,會(huì)繼承 AQS 來實(shí)現(xiàn)對應(yīng)場景的功能Sync 有兩個(gè)具體的實(shí)現(xiàn)類,分別是:
NofairSync:表示可以存在搶占鎖的功能创译,也就是說不管當(dāng)前隊(duì)列上是否存在其他線程等待抵知,新線程都有機(jī)會(huì)搶占鎖
FailSync: 表示所有線程嚴(yán)格按照 FIFO 來獲取鎖
NofairSync.lock
以非公平鎖為例,來看看 lock 中的實(shí)現(xiàn)
非公平鎖和公平鎖最大的區(qū)別在于,在非公平鎖中我搶占鎖的邏輯是刷喜,不管有沒有線程排隊(duì)残制,我先上來 cas 去搶占一下
CAS 成功,就表示成功獲得了鎖
CAS 失敗掖疮,調(diào)用 acquire(1)走鎖競爭邏輯
final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread. * currentThread * ());
else {
acquire(1);
}
}
CAS 的實(shí)現(xiàn)原理
protected final boolean compareAndSetState(int expect, int update) {
return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}
通過 cas 樂觀鎖的方式來做比較并替換初茶,這段代碼的意思是,如果當(dāng)前內(nèi)存中的state 的值和預(yù)期值 expect 相等浊闪,則替換為 update纺蛆。更新成功返回 true,否則返回 false.這個(gè)操作是原子的规揪,不會(huì)出現(xiàn)線程安全問題,這里面涉及到Unsafe這個(gè)類的操作温峭,
以及涉及到 state 這個(gè)屬性的意義猛铅。state 是 AQS 中的一個(gè)屬性,它在不同的實(shí)現(xiàn)中所表達(dá)的含義不一樣凤藏,對于重入
鎖的實(shí)現(xiàn)來說奸忽,表示一個(gè)同步狀態(tài)。它有兩個(gè)含義的表示
當(dāng) state=0 時(shí)揖庄,表示無鎖狀態(tài)
當(dāng) state>0 時(shí)栗菜,表示已經(jīng)有線程獲得了鎖,也就是 state=1蹄梢,但是因?yàn)?/p>
ReentrantLock 允許重入疙筹,所以同一個(gè)線程多次獲得同步鎖的時(shí)候,state 會(huì)遞增禁炒,比如重入 5 次而咆,那么 state=5。而在釋放鎖的時(shí)候幕袱,同樣需要釋放 5 次直到 state=0其他線程才有資格獲得鎖Unsafe 類Unsafe 類是在 sun.misc 包下暴备,不屬于 Java 標(biāo)準(zhǔn)。但是很多 Java 的基礎(chǔ)類庫们豌,包括一些被廣泛使用的高性能開發(fā)庫都是基于 Unsafe 類開發(fā)的涯捻,比如 Netty、Hadoop望迎、Kafka 等障癌;Unsafe 可認(rèn)為是 Java 中留下的后門,提供了一些低層次操作擂煞,如直接內(nèi)存訪問混弥、線程的掛起和恢復(fù)、CAS、線程同步蝗拿、內(nèi)存屏障而 CAS 就是 Unsafe 類中提供的一個(gè)原子操作晾捏,第一個(gè)參數(shù)為需要改變的對象,第二個(gè)為偏移量(即之前求出來的 headOffset 的值)哀托,第三個(gè)參數(shù)為期待的值惦辛,第
四個(gè)為更新后的值整個(gè)方法的作用是如果當(dāng)前時(shí)刻的值等于預(yù)期值 var4 相等,則更新為新的期望值 var5仓手,如果更新成功胖齐,則返回 true,否則返回 false嗽冒;
stateOffset
一個(gè) Java 對象可以看成是一段內(nèi)存呀伙,每個(gè)字段都得按照一定的順序放在這段內(nèi)存里,通過這個(gè)方法可以準(zhǔn)確地告訴你某個(gè)字段相對于對象的起始內(nèi)存地址的字節(jié)偏移添坊。用于在后面的 compareAndSwapInt 中剿另,去根據(jù)偏移量找到對象在內(nèi)存中的具體位置
所以 stateOffset 表示 state 這個(gè)字段在 AQS 類的內(nèi)存中相對于該類首地址的偏移量
compareAndSwapInt
在 unsafe.cpp 文件中,可以找到 compareAndSwarpInt 的實(shí)現(xiàn)
UNSAFE_ENTRY(jboolean, Unsafe_CompareAndSwapInt(JNIEnv *env, jobject unsafe, jobject obj, jlong offset, jint e, jint x))
UnsafeWrapper("Unsafe_CompareAndSwapInt");
oop p = JNIHandles::resolve(obj); //將 Java 對象解析成 JVM 的 oop(普通對象指針),
jint* addr = (jint *) index_oop_from_field_offset_long(p, offset); //根據(jù)對象 p 和地址偏移量找到地址
return (jint)(Atomic::cmpxchg(x, addr, e)) == e; //基于 cas 比較并替換贬蛙, x 表示需要更新的值雨女,addr 表示 state
在內(nèi)存中的地址,e 表示預(yù)期值UNSAFE_END
AQS.accquire
acquire 是 AQS 中的方法阳准,如果 CAS 操作未能成功氛堕,說明 state 已經(jīng)不為 0,此時(shí)繼續(xù) acquire(1)操作
? 大家思考一下野蝇,acquire 方法中的 1 的參數(shù)是用來做什么呢讼稚?
這個(gè)方法的主要邏輯是
通過 tryAcquire 嘗試獲取獨(dú)占鎖,如果成功返回 true浪耘,失敗返回 false
如果 tryAcquire 失敗乱灵,則會(huì)通過 addWaiter 方法將當(dāng)前線程封裝成 Node 添加到 AQS 隊(duì)列尾部
acquireQueued,將 Node 作為參數(shù)七冲,通過自旋去嘗試獲取鎖痛倚。
public final void acquire(int arg) {
if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
NonfairSync.tryAcquire
}
這個(gè)方法的作用是嘗試獲取鎖,如果成功返回 true蝉稳,不成功返回 false它是重寫 AQS 類中的 tryAcquire 方法,并且大家仔細(xì)看一下 AQS 中 tryAcquire方法的定義掘鄙,并沒有實(shí)現(xiàn)耘戚,而是拋出異常。按照一般的思維模式操漠,既然是一個(gè)不實(shí)現(xiàn)的模版方法收津,那應(yīng)該定義成 abstract饿这,讓子類來實(shí)現(xiàn)呀?大家想想為什么
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
ReentrantLock.nofairTryAcquire
獲取當(dāng)前線程撞秋,判斷當(dāng)前的鎖的狀態(tài)
如果 state=0 表示當(dāng)前是無鎖狀態(tài)长捧,通過 cas 更新 state 狀態(tài)的值
當(dāng)前線程是屬于重入,則增加重入次數(shù)
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();//獲取當(dāng)前執(zhí)行的線程
int c = getState();//獲得 state 的值
if(c ==0) {//表示無鎖狀態(tài)
if (compareAndSetState(0, acquires)) {//cas 替換 state 的值吻贿,cas 成功表示獲取鎖成功
setExclusiveOwnerThread(current);//保存當(dāng)前獲得鎖的線程,下次再來的時(shí)候不要再嘗試競爭鎖return true;
}
}
else if(current ==getExclusiveOwnerThread()){//如果同一個(gè)線程來獲得鎖串结,直接增加重入次數(shù)
int nextc = c + acquires;
if (nextc < 0) *// overflow*
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
AQS.addWaiter
當(dāng) tryAcquire 方法獲取鎖失敗以后,則會(huì)先調(diào)用 addWaiter 將當(dāng)前線程封裝成Node.入?yún)?mode 表示當(dāng)前節(jié)點(diǎn)的狀態(tài)舅列,傳遞的參數(shù)是Node.EXCLUSIVE肌割,表示獨(dú)占狀態(tài)。意味著重入鎖用到了 AQS 的獨(dú)占鎖功能
將當(dāng)前線程封裝成 Node
當(dāng)前鏈表中的 tail 節(jié)點(diǎn)是否為空帐要,如果不為空把敞,則通過 cas 操作把當(dāng)前線程的node 添加到 AQS 隊(duì)列
如果為空或者 cas 失敗,調(diào)用 enq 將節(jié)點(diǎn)添加到 AQS 隊(duì)列
private Node addWaiter(Node mode) {
Node node = new Node(Thread. * currentThread * (), mode);//把當(dāng)前線程封裝為 Node
Node pred = tail; //tail 是 AQS 中表示同比隊(duì)列隊(duì)尾的屬性榨惠,默認(rèn)是 null
if (pred != null) {//tail 不為空的情況下先巴,說明隊(duì)列中存在節(jié)點(diǎn)
node.prev = pred;//把當(dāng)前線程的 Node 的 prev 指向 tail
if (compareAndSetTail(pred, node)) {//通過 cas 把 node加入到 AQS 隊(duì)列,也就是設(shè)置為 tail
pred.next = node;//設(shè)置成功以后冒冬,把原 tail 節(jié)點(diǎn)的 next指向當(dāng)前 node
return node;
}
}
enq(node);//tail=null,把 node 添加到同步隊(duì)列
return node;
}
// enq 就是通過自旋操作把當(dāng)前節(jié)點(diǎn)加入到隊(duì)列中
private Node enq(final Node node) {
for (; ; ) {
Node t = tail;
if (t == null) { *// Must initialize*
if (compareAndSetHead(new Node()))
tail = head;
} else {
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
圖解分析
假設(shè) 3 個(gè)線程來爭搶鎖,那么截止到 enq 方法運(yùn)行結(jié)束之后摩渺,或者調(diào)用 addwaiter方法結(jié)束后简烤,AQS 中的鏈表結(jié)構(gòu)圖
AQS.acquireQueued
通過 addWaiter 方法把線程添加到鏈表后,會(huì)接著把 Node 作為參數(shù)傳遞給
acquireQueued 方法摇幻,去競爭鎖
獲取當(dāng)前節(jié)點(diǎn)的 prev 節(jié)點(diǎn)
如果 prev 節(jié)點(diǎn)為 head 節(jié)點(diǎn)横侦,那么它就有資格去爭搶鎖,調(diào)用 tryAcquire 搶占鎖
搶占鎖成功以后绰姻,把獲得鎖的節(jié)點(diǎn)設(shè)置為 head枉侧,并且移除原來的初始化 head節(jié)點(diǎn)
如果獲得鎖失敗,則根據(jù) waitStatus 決定是否需要掛起線程
最后狂芋,通過 cancelAcquire 取消獲得鎖的操作
NofairSync.tryAcquire
這個(gè)方法在前面分析過榨馁,就是通過 state 的狀態(tài)來判斷是否處于無鎖狀態(tài),然后在通過 cas 進(jìn)行競爭鎖操作帜矾。成功表示獲得鎖翼虫,失敗表示獲得鎖失敗
shouldParkAfterFailedAcquire
如果 ThreadA 的鎖還沒有釋放的情況下,ThreadB 和 ThreadC 來爭搶鎖肯定是會(huì)失敗屡萤,那么失敗以后會(huì)調(diào)用 shouldParkAfterFailedAcquire 方法Node 有 5 中狀態(tài)珍剑,分別是:CANCELLED(1),SIGNAL(-1)死陆、CONDITION(- 2)毙籽、PROPAGATE(-3)澳迫、默認(rèn)狀態(tài)(0) CANCELLED: 在同步隊(duì)列中等待的線程等待超時(shí)或被中斷阀趴,需要從同步隊(duì)列中取消該 Node 的結(jié)點(diǎn), 其結(jié)點(diǎn)的 waitStatus 為 CANCELLED漠烧,即結(jié)束狀態(tài),進(jìn)入該狀態(tài)后的結(jié)點(diǎn)將不會(huì)再變化SIGNAL: 只要前置節(jié)點(diǎn)釋放鎖浴滴,就會(huì)通知標(biāo)識(shí)為 SIGNAL 狀態(tài)的后續(xù)節(jié)點(diǎn)的線程CONDITION: 和 Condition 有關(guān)系,后續(xù)會(huì)講解PROPAGATE:共享模式下,PROPAGATE 狀態(tài)的線程處于可運(yùn)行狀態(tài)0:初始狀態(tài)這個(gè)方法的主要作用是菌羽,通過 Node 的狀態(tài)來判斷,ThreadA 競爭鎖失敗以后是否應(yīng)該被掛起由缆。
如果 ThreadA 的 pred 節(jié)點(diǎn)狀態(tài)為 SIGNAL注祖,那就表示可以放心掛起當(dāng)前線程
通過循環(huán)掃描鏈表把 CANCELLED 狀態(tài)的節(jié)點(diǎn)移除
修改 pred 節(jié)點(diǎn)的狀態(tài)為 SIGNAL,返回 false.
返回 false 時(shí),也就是不需要掛起均唉,返回 true是晨,則需要調(diào)用 parkAndCheckInterrupt掛起當(dāng)前線程
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus;//前置節(jié)點(diǎn)的waitStatus
if (ws == Node.SIGNAL)//如果前置節(jié)點(diǎn)為 SIGNAL,意味著只需要等待其他前置節(jié)點(diǎn)的線程被釋放舔箭,
return true;//返回 true罩缴,意味著可以直接放心的掛起了
if (ws > 0) {//ws 大于 0,意味著 prev 節(jié)點(diǎn)取消了排隊(duì)层扶,直接移除這個(gè)節(jié)點(diǎn)就行
do {
node.prev = pred = pred.prev;//相當(dāng)于: pred=pred.prev;
node.prev = pred;
} while (pred.waitStatus > 0); //這里采用循環(huán)箫章,從雙向列表中移除 CANCELLED 的節(jié)點(diǎn)
pred.next = node;
} else {//利用 cas 設(shè)置 prev 節(jié)點(diǎn)的狀態(tài)為 SIGNAL(-1)
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
parkAndCheckInterrupt
使用 LockSupport.park 掛起當(dāng)前線程編程 WATING 狀態(tài)Thread.interrupted,返回當(dāng)前線程是否被其他線程觸發(fā)過中斷請求镜会,也就是thread.interrupt(); 如果有觸發(fā)過中斷請求檬寂,那么這個(gè)方法會(huì)返回當(dāng)前的中斷標(biāo)識(shí)true,并且對中斷標(biāo)識(shí)進(jìn)行復(fù)位標(biāo)識(shí)已經(jīng)響應(yīng)過了中斷請求戳表。如果返回 true桶至,意味著在 acquire 方法中會(huì)執(zhí)行 selfInterrupt()。
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);
return Thread.interrupted();
}
// selfInterrupt:標(biāo)識(shí)如果當(dāng)前線程在 acquireQueued中被中斷過匾旭,則需要產(chǎn)生一 個(gè)中斷請求镣屹,原因是線程在調(diào)用 acquireQueued方法的時(shí)候是不會(huì)響應(yīng)中斷請求的
static void selfInterrupt() {
Thread.currentThread().interrupt();
}
鎖的釋放流程
如果這個(gè)時(shí)候 ThreadA 釋放鎖了,那么我們來看鎖被釋放后會(huì)產(chǎn)生什么效果
ReentrantLock.unlock
在 unlock 中价涝,會(huì)調(diào)用 release 方法來釋放鎖
public final boolean release(int arg) {
if (tryRelease(arg)) { //釋放鎖成功
Node h = head; //得到 aqs 中 head 節(jié)點(diǎn)
if (h != null && h.waitStatus != 0)//如果 head 節(jié)點(diǎn)不為空并且狀態(tài)女蜈!=0. 調(diào)用 unparkSuccessor (h) 喚醒后續(xù)節(jié)點(diǎn)
unparkSuccessor(h);
return true;
}
return false;
}
ReentrantLock.tryRelease
這個(gè)方法可以認(rèn)為是一個(gè)設(shè)置鎖狀態(tài)的操作,通過將 state 狀態(tài)減掉傳入的參數(shù)值(參數(shù)是 1)色瘩,如果結(jié)果狀態(tài)為 0鞭光,就將排它鎖的Owner 設(shè)置為 null,以使得其它的線程有機(jī)會(huì)進(jìn)行執(zhí)行泞遗。
在排它鎖中惰许,加鎖的時(shí)候狀態(tài)會(huì)增加 1(當(dāng)然可以自己修改這個(gè)值),在解鎖的時(shí)候減掉 1史辙,同一個(gè)鎖汹买,在可以重入后佩伤,可能會(huì)被疊加為 2、3晦毙、4 這些值生巡,只有 unlock()的次數(shù)與 lock()的次數(shù)對應(yīng)才會(huì)將 Owner 線程設(shè)置為空,而且也只有這種情況下才會(huì)返回 true见妒。
protected final boolean tryRelease(int releases) {
int c = getState() - releases;
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}
// unparkSuccessorprivate
void unparkSuccessor(Node node) {
int ws = node.waitStatus;//獲得 head 節(jié)點(diǎn)的狀態(tài)
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);// 設(shè)置 head 節(jié)點(diǎn)狀態(tài)為 0
Node s = node.next;//得到 head 節(jié)點(diǎn)的下一個(gè)節(jié)點(diǎn)
if (s == null || s.waitStatus > 0) {
//如果下一個(gè)節(jié)點(diǎn)為 null 或者 status>0 表示 cancelled 狀態(tài).
//通過從尾部節(jié)點(diǎn)開始掃描孤荣,找到距離 head 最近的一個(gè)waitStatus <= 0 的節(jié)點(diǎn) s = null;
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
s = t;
}
if (s != null) //next 節(jié)點(diǎn)不為空,直接喚醒這個(gè)線程即可
LockSupport.unpark(s.thread);
}
為什么在釋放鎖的時(shí)候是從 tail 進(jìn)行掃描
我們再回到 enq那個(gè)方法须揣。
將新的節(jié)點(diǎn)的 prev 指向 tail
通過 cas 將 tail 設(shè)置為新的節(jié)點(diǎn)盐股,因?yàn)?cas 是原子操作所以能夠保證線程安全性
t.next=node;設(shè)置原 tail 的 next 節(jié)點(diǎn)指向新的節(jié)點(diǎn)
private Node enq(final Node node) {
for (; ; ) {
Node t = tail;
if (t == null) { // Must initialize
if (compareAndSetHead(new Node()))
tail = head;
} else {
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
在 cas 操作之后耻卡,t.next=node 操作之前疯汁。存在其他線程調(diào)用 unlock 方法從 head開始往后遍歷,由于 t.next=node 還沒執(zhí)行意味著鏈表的關(guān)系還沒有建立完整卵酪。就會(huì)導(dǎo)致遍歷到 t 節(jié)點(diǎn)的時(shí)候被中斷幌蚊。所以從后往前遍歷,一定不會(huì)存在這個(gè)問題溃卡。
圖解分析
通過鎖的釋放溢豆,原本的結(jié)構(gòu)就發(fā)生了一些變化。head 節(jié)點(diǎn)的 waitStatus 變成了 0瘸羡,ThreadB 被喚醒
原本掛起的線程繼續(xù)執(zhí)行
通過 ReentrantLock.unlock沫换,原本掛起的線程被喚醒以后繼續(xù)執(zhí)行,應(yīng)該從哪里執(zhí)行大家還有印象吧最铁。 原來被掛起的線程是在acquireQueued 方法中,所以被喚醒以后繼續(xù)從這個(gè)方法開始執(zhí)行
AQS.acquireQueued
這個(gè)方法前面已經(jīng)完整分析過了垮兑,我們只關(guān)注一下 ThreadB 被喚醒以后的執(zhí)行流程冷尉。由于 ThreadB 的 prev 節(jié)點(diǎn)指向的是 head,并且 ThreadA 已經(jīng)釋放了鎖系枪。所以這個(gè)時(shí)候調(diào)用 tryAcquire 方法時(shí)雀哨,可以順利獲取到鎖
把 ThreadB 節(jié)點(diǎn)當(dāng)成 head
把原 head 節(jié)點(diǎn)的 next 節(jié)點(diǎn)指向?yàn)?null
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (; ; ) {
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
圖解分析
設(shè)置新 head 節(jié)點(diǎn)的 prev=null
-
設(shè)置原 head 節(jié)點(diǎn)的 next 節(jié)點(diǎn)為 null
公平鎖和非公平鎖的區(qū)別
鎖的公平性是相對于獲取鎖的順序而言的,如果是一個(gè)公平鎖私爷,那么鎖的獲取順序就應(yīng)該符合請求的絕對時(shí)間順序雾棺,也就是 FIFO。 在上面分析的例子來說衬浑,只要CAS 設(shè)置同步狀態(tài)成功捌浩,則表示當(dāng)前線程獲取了鎖,而公平鎖則不一樣工秩,差異點(diǎn)有兩個(gè)
FairSync.tryAcquire
final void lock() {
acquire(1);
}
非公平鎖在獲取鎖的時(shí)候尸饺,會(huì)先通過 CAS 進(jìn)行搶占进统,而公平鎖則不會(huì)FairSync.tryAcquire
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (!hasQueuedPredecessors() && compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
} else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
這個(gè)方法與 nonfairTryAcquire(int acquires)比較,不同的地方在于判斷條件多了hasQueuedPredecessors()方法浪听,也就是加入了[同步隊(duì)列中當(dāng)前節(jié)點(diǎn)是否有前驅(qū)節(jié)點(diǎn)]的判斷螟碎,如果該方法返回 true,則表示有線程比當(dāng)前線程更早地請求獲取鎖迹栓,因此需要等待前驅(qū)線程獲取并釋放鎖之后才能繼續(xù)獲取鎖掉分。