揭秘Java多線程:synchronized如何調(diào)度王妃與王的舉案齊眉
我是「大王」,是一人之下萬人之上的真男人!每次只能有一個(gè)王妃獲得寵信與我夜夜笙歌。
王妃們會(huì)想方設(shè)法爭(zhēng)寵獲得與我共眠的機(jī)會(huì),所以我需要一個(gè)總管來幫我合理的調(diào)度挑選一個(gè)「王妃」,他的名字叫 synchronized。
假如 synchronized 是「王」身邊的「大總管」,那么 Thread 就像是他后宮的王妃。
今日聽「碼哥」胡言亂語解開 synchronized 大總管如何調(diào)度「王妃」獲取與王夜夜笙歌的陪伴。
王妃不同狀態(tài)的變化到底經(jīng)歷了什么?且看 synchronized 大總管又采取了哪些手段更加高效調(diào)度一個(gè)王妃,宮斗還有 30 秒到達(dá)戰(zhàn)場(chǎng)?。?!
故事的開端
「碼哥」通過幾個(gè)故事,讓讀者朋友完全掌握 synchronized 的鎖優(yōu)化(偏向鎖 -> 輕量級(jí)鎖 -> 重量級(jí)鎖)原理以及線程在 6 種狀態(tài)之間轉(zhuǎn)換的奧秘。
抽象出三個(gè)概念:Thread 對(duì)應(yīng)后宮佳麗「王妃」,synchronized 是后宮大總管負(fù)責(zé)調(diào)度王妃獲得與「王」陪伴的機(jī)會(huì),「王」則是被王妃們想要競(jìng)爭(zhēng)的資源。
當(dāng)然整個(gè)「皇宮」就是 Java JVM 虛擬機(jī)。
圖片
王妃的 6 種狀態(tài)
后宮佳麗等級(jí)森嚴(yán),王妃們?cè)谶@場(chǎng)權(quán)貴的游戲中每個(gè)人的目的都是為獲取「王」寵愛,在這場(chǎng)宮斗劇中自身的狀態(tài)也隨著不同的遭遇有著不同的變化。
「Thread 王妃」的生命周期中一共有 6 種狀態(tài)。
- New(新入宮):Thread state for a thread which has not yet started.
- Runnable 可運(yùn)行、就緒:(身體舒適,準(zhǔn)備好了),Java 中的 Runable 狀態(tài)對(duì)應(yīng)操作系統(tǒng)線程狀態(tài)中的兩種狀態(tài),分別是 Running 和 Ready,也就是說,Java 中處于 Runnable 狀態(tài)的線程有可能正在執(zhí)行,也有可能沒有正在執(zhí)行,正在等待被分配 CPU 資源。
- Blocked 阻塞(身體欠佳、被打入冷宮、來大姨媽了……)
- WAITING(等待):(等待傳喚)
- Timed Waiting(計(jì)時(shí)等待):在門外計(jì)時(shí)等待
- Terminated(終止):嗝屁
圖片
線程狀態(tài)
王妃在任意時(shí)刻只能是其中的一種狀態(tài),通過 getState() 方法獲取線程狀態(tài)。
New 新入宮
第一日
「王」微服私訪,駕車游玩,途徑桃花源。見風(fēng)景宜人,如人間仙境。停車坐愛楓林晚,霜葉紅于二月花。
此時(shí)此刻,一女子媚眼含羞合,丹唇逐笑開。風(fēng)卷葡萄帶,日照石榴裙。
「王」擬寫一份招入宮的詔書,上面寫著new Thread() ,「香妃 Thread」的名分便正式成立。
New 表示線程被創(chuàng)建但是還沒有啟動(dòng)的狀態(tài),猶如「香妃」剛剛?cè)雽m,等待她后面的路程將是驚心動(dòng)魄,蕩氣回腸。
皇宮(可以理解是 JVM)命令 synchronized 大總管為「香妃 Thread」分配寢宮(也就是分配內(nèi)存),并初始化其身邊的「丫鬟」(初始化成員變量的值)。
Runnable 可運(yùn)行、就緒
「香妃 Thread」安排好衣食住行之后,便準(zhǔn)備好陪伴王了。
但是后宮佳麗很多,并不是所有人都能獲得陪伴權(quán),「香妃」早已準(zhǔn)備好,也在爭(zhēng)取可以獲得與「王」共舞的機(jī)會(huì)。
便主動(dòng)告知 synchronized 大總管,自己琴棋書畫樣樣精通,并塞給它一個(gè)紅包,希望得到安排。
JVM 安排丫鬟為「香妃 Thread」沐浴更衣,抹上胭脂等待召喚(相當(dāng)于線程的 start() 方法被調(diào)用)。
Java 虛擬機(jī)會(huì)為其創(chuàng)建方法調(diào)用??沙绦蛴?jì)數(shù)器,等到調(diào)度運(yùn)行,此刻線程就處于可運(yùn)行狀態(tài)。
Java 中的 Runable 狀態(tài)對(duì)應(yīng)操作系統(tǒng)線程狀態(tài)中的兩種狀態(tài),分別是 Running 和 Ready,也就是說,Java 中處于 Runnable 狀態(tài)的線程有可能正在執(zhí)行,也有可能沒有正在執(zhí)行,正在等待被分配 CPU 資源。
注意:?jiǎn)?dòng)線程使用 start() 方法,而不是 run() 方法。
調(diào)用 start()方法啟動(dòng)線程,系統(tǒng)會(huì)把該 run 方法當(dāng)成方法執(zhí)行體處理。
需要切記的是:調(diào)用了線程的 run()方法之后,該線程就不在處于新建狀態(tài),不要再次調(diào)用 start()方法,只能對(duì)新建狀態(tài)的線程調(diào)用start()方法,否則會(huì)引發(fā) IllegaIThreadStateExccption 異常。
「香妃 Thread」沐浴更衣之后(被調(diào)用start() )便焚香撫琴,可「淑妃 Thread」不跟示弱起舞弄影競(jìng)爭(zhēng)陪伴權(quán)。
「香妃 Thread」畢竟新來的,喜新厭舊的渣男「王」甚是寵愛,「香妃 Thread」獲得了今晚的陪伴權(quán),獲得 JVM 給予 CPU 分片后,執(zhí)行 run()方法,該方法核心功能就是造娃…..
在造娃之前,「香妃 Thread」經(jīng)歷了很多紛爭(zhēng),狀態(tài)也一直在變化。稍有不慎,可能會(huì)進(jìn)入 TERMINATED 狀態(tài),直接玩完。
請(qǐng)繼續(xù)閱讀……
Waiting 等待、Timed Waiting 計(jì)時(shí)等待、Blocked 阻塞
「淑妃 Thread」也想獲得「王」的寵幸。那晚,她想與「王」造娃,王有要事需要處理,synchronized 大總管使用了 Object.wait() 技能卡,「香妃」只能等待王回來……
王處理完要事之后,synchronized 大總管使用 Object.notify() 解鎖,通知「香妃 Thread」可以一起和「王」么么噠了,此刻「香妃 Thread」竟然來大姨媽觸發(fā)了 Thread.join() 只好去上廁所,讓老王稍等片刻。
好不容易「香妃 Thread」處于 Runnable 態(tài),但是被總管施展了 LockSupport.park() 技能卡,導(dǎo)致無法進(jìn)入寢宮,狀態(tài)由 Runnable 變成了 Waiting 態(tài)。
「咔妃 Thread」由于太黑了,直接被 synchronized 大總管拒之門外,從 Runnable 變成 Blocked。
還有其他「妃」她們分別被以下技能卡命中進(jìn)入,直接進(jìn)入 TIMED_WAITING 狀態(tài) ,一直等待有機(jī)會(huì)才能與王夜夜笙歌:
- Thread.sleep:
- Object.wait with timeout
- Thread.join with timeout
- LockSupport.parkNanos
- LockSupport.parkUntil
第二日
「咔妃」去韓國整容,變白了,得到了 syncronized 大總管的許可,得到一把叫 monitor 的令牌,由原先的 Blocked 變成了 Runnable ……
另外,有的王妃為了獲得陪伴權(quán),或者想掌管后宮。陰謀詭計(jì)被識(shí)破,被判處 Terminated 刑罰,滅頂之災(zāi),強(qiáng)擼灰飛煙滅!
synchronized 總管如何提升效率翻牌
王妃們除了使用 LockSupport.unpark() 技能卡等獲取陪伴權(quán),還可以通過由欽點(diǎn)大總管 synchronized 的令牌陪伴權(quán)。
面對(duì)三千佳麗,大總管必須要提高效率,不然將會(huì)累死而選不出一個(gè)王妃去陪伴老王,這可是要?dú)㈩^的。
因?yàn)樵?Java 5 版本之前,synchronized 的篩選方法效率很差,一堆王妃跑進(jìn)來吵著我行我上,秩序混亂,上一任總管就被殺頭了……
到了第 6 任 synchronized ,做了很大改善。運(yùn)用了自適應(yīng)自旋、鎖消除、鎖粗化、輕量級(jí)鎖、偏向鎖,效率大大提升。
自適應(yīng)自旋
synchronized 通知王妃們過來排隊(duì),「王」有急事需要處理,為了讓當(dāng)前申請(qǐng)陪伴的咖妃“稍等一下”, synchronized 大總管會(huì)讓王妃自旋,少許的等待是值得的,「王」很快就會(huì)處理完成事情。
咖妃只需要每隔一段時(shí)間詢問大總管「王」是否處理好事情,一旦『王』歸來,那么自己就不需要進(jìn)入阻塞態(tài),獲得今日與王為伴。
避免因?yàn)橐ネㄖ鄠€(gè)王妃來競(jìng)爭(zhēng)費(fèi)時(shí)費(fèi)力。
用一句話總結(jié)自旋鎖的好處,那就是自旋鎖用循環(huán)去不停地嘗試獲取鎖,讓線程始終處于 Runnable 狀態(tài),節(jié)省了線程狀態(tài)切換帶來的開銷。
以下是自旋與非自旋獲取鎖的過程:
圖片
自旋與非自旋
AtomicInteger
在 Java 1.5 版本及以上的并發(fā)包中,也就是 java.util.concurrent 的包中,里面的原子類基本都是自旋鎖的實(shí)現(xiàn)。我們看下 AtomicInteger 類的定義:
public class AtomicInteger extends Number implements java.io.Serializable {
private static final long serialVersionUID = 6214790243416807050L;
// setup to use Unsafe.compareAndSwapInt for updates
private static final Unsafe unsafe = Unsafe.getUnsafe();
private static final long valueOffset;
static {
try {
valueOffset = unsafe.objectFieldOffset
(AtomicInteger.class.getDeclaredField("value"));
} catch (Exception ex) { throw new Error(ex); }
}
private volatile int value;
......
}
各屬性的作用:
- unsafe:獲取并操作內(nèi)存的數(shù)據(jù)。
- valueOffset:存儲(chǔ) value 在 AtomicInteger 中的偏移量。
- value:存儲(chǔ) AtomicInteger 的 int 值,該屬性需要借助 volatile 關(guān)鍵字保證其在線程間是可見的。
查看 AtomicInteger 的自增函數(shù) incrementAndGet() 的源碼時(shí),自增函數(shù)底層調(diào)用的是 unsafe.getAndAddInt()。
但是由于 JDK 本身只有 Unsafe.class,只通過 class 文件中的參數(shù)名,并不能很好的了解方法的作用,我們通過 OpenJDK 8 來查看 Unsafe 的源碼:
// JDK AtomicInteger 自增
public final int getAndIncrement() {
return unsafe.getAndAddInt(this, valueOffset, 1);
}
// OpenJDK 8
// Unsafe.java
public final int getAndAddInt(Object o, long offset, int delta) {
int v;
do {
v = getIntVolatile(o, offset);
} while (!compareAndSwapInt(o, offset, v, v + delta));
return v;
}
通過 do while 實(shí)現(xiàn)了自旋,getAndAddInt() 循環(huán)獲取給定對(duì)象 o 中的偏移量處的值 v,然后判斷內(nèi)存值是否等于 v。
如果相等則將內(nèi)存值設(shè)置為 v + delta,否則返回 false,繼續(xù)循環(huán)進(jìn)行重試,直到設(shè)置成功才能退出循環(huán),并且將舊值返回。
整個(gè)“比較 + 更新”操作封裝在 compareAndSwapInt() 中,在 JNI 里是借助于一個(gè) CPU 指令完成的,屬于原子操作,可以保證多個(gè)線程都能夠看到同一個(gè)變量的修改值。
synchronized 大總管在 1.6 版本想出了自適應(yīng)自旋鎖來解決長(zhǎng)時(shí)間自旋的問題,防止一直傻傻等待傻傻的問。會(huì)根據(jù)最近自旋的成功率、失敗率。
如果最近嘗試自旋獲取某一把鎖成功了,那么下一次可能還會(huì)繼續(xù)使用自旋,并且允許自旋更長(zhǎng)的時(shí)間;
但是如果最近自旋獲取某一把鎖失敗了,那么可能會(huì)省略掉自旋的過程,以便減少無用的自旋,提高效率。
鎖消除
淑妃詭詐,在公元 107 年一個(gè)夜黑風(fēng)高的夜晚,串通后廚小哲子放了無色無味的黯然銷魂藥,妃子們有氣無力。
所以只剩下自己一個(gè)人向 synchronized 大總管申請(qǐng)與「王」共眠的機(jī)會(huì),不需要繁瑣流程,直搗黃龍。直接面見老王,無需加鎖。
鎖消除即刪除不必要的加鎖操作。虛擬機(jī)即時(shí)編輯器在運(yùn)行時(shí),對(duì)一些“代碼上要求同步,但是被檢測(cè)到不可能存在共享數(shù)據(jù)競(jìng)爭(zhēng)”的鎖進(jìn)行消除。
根據(jù)代碼逃逸技術(shù),如果判斷到一段代碼中,堆上的數(shù)據(jù)不會(huì)逃逸出當(dāng)前線程,那么可以認(rèn)為這段代碼是線程安全的,不必要加鎖。
public class SynchronizedTest {
public static void main(String[] args) {
SynchronizedTest test = new SynchronizedTest();
for (int i = 0; i < 100000000; i++) {
test.append("碼哥字節(jié)", "def");
}
}
public void append(String str1, String str2) {
StringBuffer sb = new StringBuffer();
sb.append(str1).append(str2);
}
}
雖然 StringBuffer 的 append 是一個(gè)同步方法,但是這段程序中的 StringBuffer 屬于一個(gè)局部變量,并且不會(huì)從該方法中逃逸出去(即 StringBuffer sb 的引用沒有傳遞到該方法外,不可能被其他線程拿到該引用),所以其實(shí)這過程是線程安全的,可以將鎖消除。
鎖粗化
「甄嬛 Thread」深得老王寵愛,被偏愛的都有恃無恐。
每次進(jìn)出 synchronized 大總管的大門都需要驗(yàn)證是否獲得 monitor 鎖,甄嬛進(jìn)來后還喜歡出去外面走走過一會(huì)有進(jìn)來看幾眼老王又出去,大總管也不用每次都要驗(yàn)證,將限制的范圍就加大了,防止反復(fù)驗(yàn)證。
如果一系列的連續(xù)操作都對(duì)同一個(gè)對(duì)象反復(fù)加鎖和解鎖,甚至加鎖操作是出現(xiàn)在循環(huán)體中的,那即使沒有出現(xiàn)線程競(jìng)爭(zhēng),頻繁地進(jìn)行互斥同步操作也會(huì)導(dǎo)致不必要的性能損耗。
如果虛擬機(jī)檢測(cè)到有一串零碎的操作都是對(duì)同一對(duì)象的加鎖,將會(huì)把加鎖同步的范圍擴(kuò)展(粗化)到整個(gè)操作序列的外部。
public class StringBufferTest {
StringBuffer stringBuffer = new StringBuffer();
public void append(){
stringBuffer.append("關(guān)注");
stringBuffer.append("公眾號(hào)");
stringBuffer.append("碼哥字節(jié)");
}
}
每次調(diào)用 stringBuffer.append 方法都需要加鎖和解鎖,如果虛擬機(jī)檢測(cè)到有一系列連串的對(duì)同一個(gè)對(duì)象加鎖和解鎖操作,就會(huì)將其合并成一次范圍更大的加鎖和解鎖操作,即在第一次 append 方法時(shí)進(jìn)行加鎖,最后一次 append 方法結(jié)束后進(jìn)行解鎖。
偏向鎖/輕量級(jí)鎖/重量級(jí)鎖
偏向鎖
老王偏愛「甄嬛」,synchronized 大總管便在一個(gè)叫 Mark Word 里柜子存儲(chǔ)鎖偏向的線程 ID,記錄著甄嬛的 ID,不需要執(zhí)行繁瑣的翻牌流程。
只需要判斷下申請(qǐng)的王妃 ID 是否跟柜子里記錄的 ID 一致。
當(dāng)一個(gè)線程訪問同步代碼塊并獲取鎖時(shí),會(huì)在 Mark Word 里存儲(chǔ)鎖偏向的線程 ID。
在線程進(jìn)入和退出同步塊時(shí)不再通過 CAS 操作來加鎖和解鎖,而是檢測(cè) Mark Word 里是否存儲(chǔ)著指向當(dāng)前線程的偏向鎖。
引入偏向鎖是為了在無多線程競(jìng)爭(zhēng)的情況下盡量減少不必要的輕量級(jí)鎖執(zhí)行路徑,因?yàn)檩p量級(jí)鎖的獲取及釋放依賴多次 CAS 原子指令,而偏向鎖只需要在置換 ThreadID 的時(shí)候依賴一次 CAS 原子指令即可。
偏向鎖只有遇到其他線程嘗試競(jìng)爭(zhēng)偏向鎖時(shí),持有偏向鎖的線程才會(huì)釋放鎖,線程不會(huì)主動(dòng)釋放偏向鎖。
偏向鎖的撤銷,需要等待全局安全點(diǎn)(在這個(gè)時(shí)間點(diǎn)上沒有字節(jié)碼正在執(zhí)行),它會(huì)首先暫停擁有偏向鎖的線程,判斷鎖對(duì)象是否處于被鎖定狀態(tài)。
撤銷偏向鎖后恢復(fù)到無鎖(標(biāo)志位為“01”)或輕量級(jí)鎖(標(biāo)志位為“00”)的狀態(tài)。
偏向鎖在 JDK 6 及以后的 JVM 里是默認(rèn)啟用的。可以通過 JVM 參數(shù)關(guān)閉偏向鎖:-XX:-UseBiasedLocking=false,關(guān)閉之后程序默認(rèn)會(huì)進(jìn)入輕量級(jí)鎖狀態(tài)。
輕量級(jí)鎖
是指當(dāng)鎖是偏向鎖的時(shí)候,被另外的線程所訪問,偏向鎖就會(huì)升級(jí)為輕量級(jí)鎖,其他線程會(huì)通過自旋的形式嘗試獲取鎖,不會(huì)阻塞,從而提高性能。
- 在代碼進(jìn)入同步塊的時(shí)候,如果同步對(duì)象鎖狀態(tài)為無鎖狀態(tài)(鎖標(biāo)志位為“01”狀態(tài),是否為偏向鎖為“0”),虛擬機(jī)首先將在當(dāng)前線程的棧幀中建立一個(gè)名為鎖記錄(Lock Record)的空間,用于存儲(chǔ)鎖對(duì)象目前的 Mark Word 的拷貝,官方稱之為 Displaced Mark Word。這時(shí)候線程堆棧與對(duì)象頭的狀態(tài)如下圖所示。
圖片
輕量級(jí)鎖
- 拷貝 Object 對(duì)象頭中的 Mark Word 復(fù)制到 LockRecord 中。
- 拷貝成功后,虛擬機(jī)將使用 CAS 操作嘗試將對(duì)象的 Mark Word 更新為指向 Lock Record 的指針,并將 Lock record 里的 owne r 指針指向 object mark word。如果更新成功,則執(zhí)行步驟 4。
- 如果這個(gè)更新動(dòng)作成功了,那么這個(gè)線程就擁有了該對(duì)象的鎖,并且對(duì)象 Mark Word 的鎖標(biāo)志位設(shè)置為“00”,即表示此對(duì)象處于輕量級(jí)鎖定狀態(tài),這時(shí)候線程堆棧與對(duì)象頭的狀態(tài)如下圖所示。
圖片
- 如果這個(gè)更新操作失敗了,虛擬機(jī)首先會(huì)檢查對(duì)象的 Mark Word 是否指向當(dāng)前線程的棧幀,如果是就說明當(dāng)前線程已經(jīng)擁有了這個(gè)對(duì)象的鎖,那就可以直接進(jìn)入同步塊繼續(xù)執(zhí)行。否則說明多個(gè)線程競(jìng)爭(zhēng)鎖,若當(dāng)前只有一個(gè)等待線程,則可通過自旋稍微等待一下,可能另一個(gè)線程很快就會(huì)釋放鎖。但是當(dāng)自旋超過一定的次數(shù),或者一個(gè)線程在持有鎖,一個(gè)在自旋,又有第三個(gè)來訪時(shí),輕量級(jí)鎖膨脹為重量級(jí)鎖,重量級(jí)鎖使除了擁有鎖的線程以外的線程都阻塞,防止 CPU 空轉(zhuǎn),鎖標(biāo)志的狀態(tài)值變?yōu)椤?0”,Mark Word 中存儲(chǔ)的就是指向重量級(jí)鎖(互斥量)的指針,后面等待鎖的線程也要進(jìn)入阻塞狀態(tài)。
重量級(jí)鎖
如上輕量級(jí)鎖的加鎖過程步驟(5),輕量級(jí)鎖所適應(yīng)的場(chǎng)景是線程近乎交替執(zhí)行同步塊的情況,如果存在同一時(shí)間訪問同一鎖的情況,就會(huì)導(dǎo)致輕量級(jí)鎖膨脹為重量級(jí)鎖。Mark Word 的鎖標(biāo)記位更新為 10,Mark Word 指向互斥量(重量級(jí)鎖)
Synchronized 的重量級(jí)鎖是通過對(duì)象內(nèi)部的一個(gè)叫做監(jiān)視器鎖(monitor)來實(shí)現(xiàn)的,監(jiān)視器鎖本質(zhì)又是依賴于底層的操作系統(tǒng)的 Mutex Lock(互斥鎖)來實(shí)現(xiàn)的。
而操作系統(tǒng)實(shí)現(xiàn)線程之間的切換需要從用戶態(tài)轉(zhuǎn)換到核心態(tài),這個(gè)成本非常高,狀態(tài)之間的轉(zhuǎn)換需要相對(duì)比較長(zhǎng)的時(shí)間,這就是為什么 Synchronized 效率低的原因。
鎖升級(jí)路徑
從無鎖到偏向鎖,再到輕量級(jí)鎖,最后到重量級(jí)鎖。結(jié)合前面我們講過的知識(shí),偏向鎖性能最好,避免了 CAS 操作。
而輕量級(jí)鎖利用自旋和 CAS 避免了重量級(jí)鎖帶來的線程阻塞和喚醒,性能中等。
重量級(jí)鎖則會(huì)把獲取不到鎖的線程阻塞,性能最差。
圖片
鎖升級(jí)
綜上,偏向鎖通過對(duì)比 Mark Word 解決加鎖問題,避免執(zhí)行 CAS 操作。而輕量級(jí)鎖是通過用 CAS 操作和自旋來解決加鎖問題,避免線程阻塞和喚醒而影響性能。重量級(jí)鎖是將除了擁有鎖的線程以外的線程都阻塞。