本篇將介紹線程安全所涉及的概念和分類横腿、同步實(shí)現(xiàn)的方式及虛擬機(jī)的底層運(yùn)作原理,以及虛擬機(jī)為了實(shí)現(xiàn)高效并發(fā)所采取的一系列鎖優(yōu)化措施咳短。
- 概述
- 線程安全
- 鎖優(yōu)化
1.概述
在要點(diǎn)提煉| 理解JVM之內(nèi)存模型&線程中主要介紹了虛擬機(jī)如何實(shí)現(xiàn)『并發(fā)』倒源,現(xiàn)在的關(guān)注點(diǎn)是虛擬機(jī)如何實(shí)現(xiàn)『高效』攘蔽。
2.線程安全
在實(shí)現(xiàn)高效之前隶症,首先需要保證并發(fā)的正確性政模,因此本節(jié)先介紹線程安全。
a.定義:當(dāng)多個(gè)線程訪問一個(gè)對(duì)象時(shí)蚂会,如果不用考慮這些線程在運(yùn)行時(shí)環(huán)境下的調(diào)度和交替執(zhí)行淋样,也不需要進(jìn)行額外的同步,或者在調(diào)用方進(jìn)行任何其他的協(xié)調(diào)操作胁住,調(diào)用這個(gè)對(duì)象的行為都可以獲得正確的結(jié)果习蓬,那這個(gè)對(duì)象是線程安全的。
要求線程安全的代碼都必須具備一個(gè)特征:
代碼本身封裝了所有必要的正確性保障手段(如互斥同步等)措嵌,令調(diào)用者無須關(guān)心多線程的問題,更無須自己采取任何措施來保證多線程的正確調(diào)用芦缰。
b.分類:按照線程安全的程度由強(qiáng)至弱分成五類
-
不可變:外部的可見狀態(tài)永遠(yuǎn)不會(huì)改變企巢,在多個(gè)線程之中永遠(yuǎn)是一致的狀態(tài)。
- 一定是線程安全的
-
如何實(shí)現(xiàn):
- 如果共享數(shù)據(jù)是一個(gè)基本數(shù)據(jù)類型让蕾,只要在定義時(shí)用
final
關(guān)鍵字修飾浪规; - 如果共享數(shù)據(jù)是一個(gè)對(duì)象,最簡單的方法是把對(duì)象中帶有狀態(tài)的變量都聲明為
final
探孝。
- 如果共享數(shù)據(jù)是一個(gè)基本數(shù)據(jù)類型让蕾,只要在定義時(shí)用
- 絕對(duì)線程安全:完全滿足之前給出的線程安全的定義笋婿,即達(dá)到“不管運(yùn)行時(shí)環(huán)境如何,調(diào)用者都不需要任何額外的同步措施”顿颅。
-
相對(duì)線程安全:能保證對(duì)該對(duì)象單獨(dú)的操作是線程安全的缸濒,在調(diào)用時(shí)無需做額外保障措施,但對(duì)于一些特定順序的連續(xù)調(diào)用粱腻,可能需要在調(diào)用端使用額外的同步措施來保證調(diào)用的正確性庇配。
- 是通常意義上所講的線程安全
- 大部分的線程安全類都屬于這種類型,如
Vector
绍些、HashTable
捞慌、Collections#synchronizedCollection()
包裝的集合... - 有關(guān)實(shí)現(xiàn)在下一小節(jié)細(xì)說。
-
線程兼容:對(duì)象本身非線程安全的柬批,但可以通過在調(diào)用端正確地使用同步手段來保證對(duì)象在并發(fā)環(huán)境中可以安全地使用啸澡,
- 是通常意義上所講的非線程安全
- Java API中大部分類都是屬于線程兼容的袖订,如
ArrayList
和HashMap
...
- 線程對(duì)立:無論調(diào)用端是否采取了同步措施,都無法在多線程環(huán)境中并發(fā)使用的代碼嗅虏。
c.線程安全的實(shí)現(xiàn)
可分成兩大手段洛姑,本篇重點(diǎn)在虛擬機(jī)本身
- 通過代碼編寫實(shí)現(xiàn)線程安全
- 通過虛擬機(jī)本身實(shí)現(xiàn)同步與鎖
①互斥同步(Mutual Exclusion&Synchronization)
-
含義:
- 同步:在多個(gè)線程并發(fā)訪問共享數(shù)據(jù)時(shí),保證共享數(shù)據(jù)在同一個(gè)時(shí)刻只被一個(gè)線程使用旋恼。
- 互斥:是實(shí)現(xiàn)同步的一種手段吏口,臨界區(qū)(Critical Section)、互斥量(Mutex)和信號(hào)量(Semaphore)都是主要的互斥實(shí)現(xiàn)方式冰更。
互斥是因产徊,同步是果;互斥是方法蜀细,同步是目的舟铜。
- 屬于悲觀并發(fā)策略,即認(rèn)為只要不做正確的同步措施就肯定會(huì)出現(xiàn)問題奠衔,因此無論共享數(shù)據(jù)是否真的會(huì)出現(xiàn)競爭谆刨,都要加鎖。
- 最大的問題是進(jìn)行線程阻塞和喚醒所帶來的性能問題归斤,也稱為阻塞同步(Blocking Synchronization)
-
手段:
- 使用
synchronized
關(guān)鍵字:-
原理:編譯后會(huì)在同步塊的前后分別形成
monitorenter
和monitorexit
這兩個(gè)字節(jié)碼指令痊夭,并通過一個(gè)reference
類型的參數(shù)來指明要鎖定和解鎖的對(duì)象。若明確指定了對(duì)象參數(shù)脏里,則取該對(duì)象的reference
她我;否則,會(huì)根據(jù)synchronized
修飾的是實(shí)例方法還是類方法去取對(duì)應(yīng)的對(duì)象實(shí)例或Class對(duì)象來作為鎖對(duì)象迫横。 -
過程:執(zhí)行
monitorenter
指令時(shí)先要嘗試獲取對(duì)象的鎖番舆。若該對(duì)象沒被鎖定或者已被當(dāng)前線程獲取,那么鎖計(jì)數(shù)器+1矾踱;而在執(zhí)行monitorexit
指令時(shí)恨狈,鎖計(jì)數(shù)器-1;當(dāng)鎖計(jì)數(shù)器=0時(shí)呛讲,鎖就被釋放禾怠;若獲取對(duì)象鎖失敗,那當(dāng)前線程會(huì)一直被阻塞等待贝搁,直到對(duì)象鎖被另外一個(gè)線程釋放為止刃宵。 -
特別注意:
synchronized
同步塊對(duì)同一條線程來說是可重入的,不會(huì)出現(xiàn)自我鎖死的問題徘公;還有牲证,同步塊在已進(jìn)入的線程執(zhí)行完之前,會(huì)阻塞后面其他線程的進(jìn)入关面。
-
原理:編譯后會(huì)在同步塊的前后分別形成
- 使用重入鎖
ReentrantLock
:-
相同:用法與
synchronized
很相似坦袍,且都可重入十厢。 - 與
synchronized
的不同:- 等待可中斷:當(dāng)持有鎖的線程長期不釋放鎖的時(shí)候,正在等待的線程可以選擇放棄等待捂齐,改為處理其他事情蛮放。
-
公平鎖:多個(gè)線程在等待同一個(gè)鎖時(shí),必須按照申請(qǐng)鎖的時(shí)間順序來依次獲得鎖奠宜。而
synchronized
是非公平的包颁,即在鎖被釋放時(shí),任何一個(gè)等待鎖的線程都有機(jī)會(huì)獲得鎖压真。ReentrantLock
默認(rèn)情況下也是非公平的娩嚼,但可以通過帶布爾值的構(gòu)造函數(shù)改用公平鎖。 -
鎖綁定多個(gè)條件:一個(gè)
ReentrantLock
對(duì)象可以通過多次調(diào)用newCondition()
同時(shí)綁定多個(gè)Condition
對(duì)象滴肿。而在synchronized
中岳悟,鎖對(duì)象的wait()
和notify()
或notifyAl()
只能實(shí)現(xiàn)一個(gè)隱含的條件,若要和多于一個(gè)的條件關(guān)聯(lián)不得不額外地添加一個(gè)鎖泼差。
-
選擇:在
synchronized
能實(shí)現(xiàn)需求的情況下贵少,優(yōu)先考慮使用它來進(jìn)行同步。下兩張圖是兩者在不同處理器上的吞吐量對(duì)比堆缘。
-
相同:用法與
- 使用
②非阻塞同步(Non-Blocking Synchronization):
- 基于沖突檢測的樂觀并發(fā)策略滔灶,即先進(jìn)行操作,若無其他線程爭用共享數(shù)據(jù)吼肥,操作成功宽气;反之產(chǎn)生了沖突再去采取其他的補(bǔ)償措施。
- 為了保證操作和沖突檢測這兩步具備原子性潜沦,需要用到硬件指令集,比如:
- 測試并設(shè)置(Test-and-Set)
- 獲取并增加(Fetch-and-Increment)
- 交換(Swap)
- 比較并交換(Compare-and-Swap,CAS)
- 加載鏈接/條件存儲(chǔ)(Load-Linked/Store-Conditional,LL/SC)
③無同步方案
- 定義:不用同步的方式保證線程安全绪氛,因?yàn)橛行┐a天生就是線程安全的唆鸡。下面舉兩個(gè)例子:
- ①可重入代碼(Reentrant Code)/純代碼(Pure Code)
- 含義:可在代碼執(zhí)行的任何時(shí)刻中斷它去執(zhí)行另外一段代碼,當(dāng)控制權(quán)返回后原來的程序并不會(huì)出現(xiàn)任何錯(cuò)誤枣察。
- 共同特征:不依賴存儲(chǔ)在堆上的數(shù)據(jù)和公用的系統(tǒng)資源争占、用到的狀態(tài)量都由參數(shù)中傳入、不調(diào)用非可重入的方法...
- 判定依據(jù):如果一個(gè)方法序目,它的返回結(jié)果是可預(yù)測的臂痕,只要輸入相同的數(shù)據(jù)就都能返回相同的結(jié)果,就滿足可重入性猿涨。
滿足可重入性的代碼一定是線程安全的握童,反之,滿足線程安全的代碼不一定是可重入的叛赚。
- ②線程本地存儲(chǔ)(Thread Local Storage)
- 含義:把共享數(shù)據(jù)的可見范圍限制在同一個(gè)線程之內(nèi)澡绩,無須同步就能保證線程之間不出現(xiàn)數(shù)據(jù)爭用的問題稽揭。
- 使用
ThreadLocal
類可實(shí)現(xiàn)線程本地存儲(chǔ)的功能:每個(gè)線程的Thread
對(duì)象中都有一個(gè)ThreadLocalMap對(duì)象,它存儲(chǔ)了一組以ThreadLocal.threadLocalHashCode為key肥卡、以本地線程變量為value的鍵值對(duì)溪掀,而ThreadLocal對(duì)象就是當(dāng)前線程的ThreadLocalMap的訪問入口,也就包含了一個(gè)獨(dú)一無二的threadLocalHashCode值步鉴,通過這個(gè)值就可以在線程鍵值值對(duì)中找回對(duì)應(yīng)的本地線程變量揪胃。
3.鎖優(yōu)化
解決并發(fā)的正確性之后,為了能在線程之間更『高效』地共享數(shù)據(jù)氛琢、解決競爭問題喊递、提高程序的執(zhí)行效率,下面介紹五種鎖優(yōu)化技術(shù)艺沼。
a.適應(yīng)性自旋(Adaptive Spinning)
- 背景:互斥同步在實(shí)現(xiàn)阻塞和喚醒時(shí)需要掛起線程和恢復(fù)線程的操作册舞,都需要轉(zhuǎn)入內(nèi)核態(tài)中完成,很影響系統(tǒng)的并發(fā)性能障般;同時(shí)调鲸,在許多應(yīng)用上共享數(shù)據(jù)的鎖定狀態(tài)只是暫時(shí),沒必要去掛起和恢復(fù)線程挽荡。
-
自旋鎖:當(dāng)物理機(jī)器有多個(gè)處理器使得多個(gè)線程同時(shí)并行執(zhí)行時(shí)藐石,先讓后請(qǐng)求鎖的線程等待,但不放棄處理器的執(zhí)行時(shí)間定拟,看看持有鎖的線程是否很快就會(huì)釋放鎖于微,這時(shí)只需讓線程執(zhí)行一個(gè)忙循環(huán),即自旋青自。
- 注意:自旋等待不能代替阻塞株依,它雖然能避免線程切換的開銷,但會(huì)占用處理器時(shí)間延窜,因此自旋等待的時(shí)間必須要有一定的限度恋腕,如果自旋超過了限定的次數(shù)仍未成功獲鎖,就需要掛線程了逆瑞。
-
自適應(yīng)自旋鎖:自旋的時(shí)間不再固定荠藤,而是由該鎖上的上次自旋時(shí)間及鎖的擁有者的狀態(tài)共同決定。具體表現(xiàn)是:
- 如果對(duì)于某個(gè)鎖获高,自旋等待剛剛成功獲得哈肖,且持有鎖的線程正在運(yùn)行中,那么虛擬機(jī)很可能允許自旋等待的時(shí)間更久點(diǎn)念秧。
- 如果對(duì)于某個(gè)鎖淤井,自旋很少成功獲得過,那么很可能以后將省略自旋等待這個(gè)鎖,避免浪費(fèi)處理器資源庄吼。
b.鎖消除(Lock Elimination)
- 鎖消除:指虛擬機(jī)即時(shí)編譯器在運(yùn)行時(shí)缎除,對(duì)一些代碼上要求同步,但是被檢測到不可能存在共享數(shù)據(jù)競爭的鎖進(jìn)行消除总寻。
- 判定依據(jù):如果一段代碼中堆上的所有數(shù)據(jù)都不會(huì)逃逸出去被其他線程訪問到器罐,可把它們當(dāng)做棧上數(shù)據(jù)對(duì)待,即線程私有的渐行,無須同步加鎖轰坊。
c.鎖粗化(Lock Coarsening)
一般情況下,會(huì)將同步塊的作用范圍限制到只在共享數(shù)據(jù)的實(shí)際作用域中才進(jìn)行同步祟印,使得需要同步的操作數(shù)量盡可能變小肴沫,保證就算存在鎖競爭,等待鎖的線程也能盡快拿到鎖蕴忆。
但如果反復(fù)操作對(duì)同一個(gè)對(duì)象進(jìn)行加鎖和解鎖颤芬,即使沒有線程競爭,頻繁地進(jìn)行互斥同步操作也會(huì)導(dǎo)致不必要的性能損耗套鹅,此時(shí)站蝠,虛擬機(jī)將會(huì)把加鎖同步的范圍粗化到整個(gè)操作序列的外部,這樣只需加一次鎖卓鹿。
d.輕量級(jí)鎖(Lightweight Locking)
- 目的:在沒有多線程競爭的前提下菱魔,減少傳統(tǒng)的重量級(jí)鎖使用操作系統(tǒng)互斥量產(chǎn)生的性能消耗,注意不是用來代替重量級(jí)鎖的吟孙。
首先先理解HotSpot虛擬機(jī)的對(duì)象頭的內(nèi)存布局:分為兩部分
- 第一部分用于存儲(chǔ)對(duì)象自身的運(yùn)行時(shí)數(shù)據(jù)澜倦,這部分被稱為Mark Word,是實(shí)現(xiàn)輕量級(jí)鎖和偏向鎖的關(guān)鍵杰妓。如哈希碼藻治、GC分代年齡等。
- 另外一部分用于存儲(chǔ)指向方法區(qū)對(duì)象類型數(shù)據(jù)的指針巷挥,如果是數(shù)組對(duì)象還會(huì)有一個(gè)額外的部分用于存儲(chǔ)數(shù)組長度桩卵。
-
加鎖過程:代碼進(jìn)入同步塊時(shí),如果同步對(duì)象未被鎖定(鎖標(biāo)志位為
01
)句各,虛擬機(jī)會(huì)在當(dāng)前線程的棧幀中建立一個(gè)名為Lock Record的空間,用于存儲(chǔ)鎖對(duì)象Mark Word的拷貝晴叨。如下圖凿宾。
之后虛擬機(jī)會(huì)嘗試用CAS操作將對(duì)象的Mark Word更新為指向Lock Record的指針。若更新動(dòng)作成功兼蕊,那么當(dāng)前線程就擁有了該對(duì)象的鎖初厚,且對(duì)象Mark Word的鎖標(biāo)志位變?yōu)?code>00,即處于輕量級(jí)鎖定狀態(tài);反之产禾,虛擬機(jī)會(huì)先檢查對(duì)象的Mark Word是否指向當(dāng)前線程的棧幀排作,若當(dāng)前線程已有該對(duì)象的鎖,可直接進(jìn)入同步塊繼續(xù)執(zhí)行亚情,否則說明改對(duì)象已被其他線程搶占妄痪。如下圖。
另外楞件,如果有兩條以上的線程爭用同一個(gè)鎖衫生,那輕量級(jí)鎖就不再有效,要膨脹為重量級(jí)鎖土浸,鎖標(biāo)志位變?yōu)?code>10罪针,Mark Word中存儲(chǔ)的就是指向重量級(jí)鎖的指針,后面等待鎖的線程也要進(jìn)入阻塞狀態(tài)黄伊。
- 解鎖過程:若對(duì)象的Mark Word仍指向著線程的Lock Record泪酱,就用CAS操作把對(duì)象當(dāng)前的Mark Word和線程中復(fù)制的Displaced Mark Word替換回來。若替換成功还最,那么就完成了整個(gè)同步過程墓阀;反之,說明有其他線程嘗試獲取該鎖憋活,那么就要在釋放鎖的同時(shí)喚醒被掛起的線程岂津。
- 優(yōu)點(diǎn):因?yàn)閷?duì)于絕大部分的鎖,在整個(gè)同步周期內(nèi)都是不存在競爭的悦即,所以輕量級(jí)鎖通過使用CAS操作消除同步使用的互斥量吮成。
e.偏向鎖(Biased Locking)
- 目的:消除數(shù)據(jù)在無競爭情況下的同步原語,進(jìn)一步提高程序的運(yùn)行性能辜梳。
- 含義:偏向鎖會(huì)偏向于第一個(gè)獲得它的線程粱甫,如果在后面的執(zhí)行中該鎖沒有被其他的線程獲取,則持有偏向鎖的線程將永遠(yuǎn)不需要再進(jìn)行同步作瞄。
-
加鎖過程:啟用偏向鎖的鎖對(duì)象在第一次被線程獲取時(shí)茶宵,Mark Word的鎖標(biāo)志位會(huì)被設(shè)置為
01
,即偏向模式宗挥,同時(shí)使用CAS操作把獲取到這個(gè)鎖的線程ID記錄在對(duì)象的Mark Word中乌庶。若操作成功,持有偏向鎖的線程以后每次進(jìn)入這個(gè)鎖相關(guān)的同步塊時(shí)都可不再進(jìn)行任何同步操作契耿。 -
解鎖過程:當(dāng)有另外的線程去嘗試獲取這個(gè)鎖時(shí)瞒大,根據(jù)鎖對(duì)象目前是否處于被鎖定的狀態(tài),撤銷偏向后恢復(fù)到未鎖定
01
或輕量級(jí)鎖定00
的狀態(tài)搪桂,后續(xù)的同步操作就如輕量級(jí)鎖執(zhí)行過程透敌。如下圖。
- 優(yōu)點(diǎn):可提高帶有同步但無競爭的程序性能,但若程序中大多數(shù)鎖總被多個(gè)線程訪問酗电,此模式就沒必要了魄藕。