線程安全與鎖優(yōu)化
1. 線程安全
按照線程安全的安全程度由強(qiáng)到弱排序,Java中各種操作共享數(shù)據(jù)分為以下5類:不可變腊凶、絕對(duì)線程安全编矾、相對(duì)線程安全、線程兼容和線程對(duì)立箱熬。
-
不可變
Java API中的不可變類有:String类垦、枚舉類、java.lang.Number的部分子類(Long城须、Double等數(shù)值包裝類型护锤,BigInteger、BigDecimal等大數(shù)據(jù)類型)酿傍。java.lang.Number的子類中的原子類(AutomicInteger烙懦、AtomicLong)是可變類。
-
絕對(duì)線程安全
絕對(duì)線程安全完全滿足Brian Goetz給出的線程安全的定義:不管運(yùn)行時(shí)環(huán)境如何赤炒,調(diào)用者都不需要任何額外的同步措施氯析。這個(gè)定義是很嚴(yán)格的,Java API中標(biāo)注是線程安全的類莺褒,大多數(shù)都不是絕對(duì)的線程安全掩缓。
-
相對(duì)線程安全
相對(duì)線程安全就是我們通常意義上所講的線程安全。它需要保證對(duì)這個(gè)對(duì)象單獨(dú)的操作是線程安全的遵岩,我們?cè)谡{(diào)用的時(shí)候不需要做額外的保障措施你辣,但對(duì)于一些特定順序的連續(xù)調(diào)用巡通,就可能需要在調(diào)用端使用額外的同步手段來保證調(diào)用的正確性。
-
線程兼容
線程兼容指對(duì)象本身并不是線程安全的舍哄,但是可以通過在調(diào)用端正確地使用同步手段來保證對(duì)象在并發(fā)環(huán)境中可以安全地使用宴凉,我們平時(shí)說一個(gè)類不是線程安全的,絕大多數(shù)指的是這種情況表悬。
-
線程對(duì)立
線程對(duì)立是指無(wú)論調(diào)用端是否采取了同步措施弥锄,都無(wú)法在多線程環(huán)境中并發(fā)使用的代碼。如Thread中被@Deprecated的suspend()蟆沫、resume()籽暇,還有System.setIn()、System.setOut()饭庞、System.runFinalizersOnExit()等戒悠。
2. 線程安全的實(shí)現(xiàn)方法
2.1 互斥同步
同步是指在多個(gè)線程并發(fā)訪問共享數(shù)據(jù)時(shí),保證共享數(shù)據(jù)在同一個(gè)時(shí)刻只被一個(gè)(或者是一些舟山,使用信號(hào)量時(shí))線程使用救崔。互斥是實(shí)現(xiàn)同步的一種手段捏顺,臨界區(qū)六孵、互斥量、信號(hào)量都是主要的互斥實(shí)現(xiàn)方式幅骄。
互斥同步時(shí)劫窒,如果獲取對(duì)象的鎖失敗,那么線程就要阻塞等待拆座,因此這種同步又叫做阻塞同步主巍。
使用synchronized與java.util.concurrent.ReentrantLock來實(shí)現(xiàn)同步。
ReentrantLock還有一些高級(jí)功能:
-
等待可中斷
等待可中斷指當(dāng)持有鎖的線程長(zhǎng)時(shí)間不釋放鎖時(shí)挪凑,正在等待的線程可以選擇放棄等待孕索,改為處理其他事情□锾迹可中斷特性對(duì)處理執(zhí)行時(shí)間非常長(zhǎng)的同步塊很有幫助搞旭。
-
可實(shí)現(xiàn)公平鎖
實(shí)現(xiàn)公平鎖指多個(gè)線程在等待同一個(gè)鎖時(shí),必須按照申請(qǐng)鎖的時(shí)間順序來依次獲得鎖菇绵,而非公平鎖則不能保證這一點(diǎn)肄渗,非公平鎖在鎖被釋放時(shí),任何一個(gè)等待鎖的線程都有機(jī)會(huì)獲得鎖咬最。synchronized鎖是非公平的翎嫡。
-
鎖可以綁定多個(gè)條件
一個(gè)ReentrantLock可以同時(shí)綁定多個(gè)Condition對(duì)象。
2.2 非阻塞同步
互斥同步(阻塞同步)最主要的問題就是進(jìn)行線程阻塞和喚醒所帶來的性能問題永乌。
基于沖突檢測(cè)的樂觀并發(fā)策略惑申,通俗講就是先進(jìn)行操作具伍,如果沒有其他線程爭(zhēng)用共享數(shù)據(jù),那操作就成功了圈驼;如果共享數(shù)據(jù)有爭(zhēng)用人芽,產(chǎn)生了沖突,就在采取其他的補(bǔ)償措施(最常見的補(bǔ)償措施就是不斷地重試碗脊,直到成功為止)啼肩,這種樂觀的并發(fā)策略的許多實(shí)現(xiàn)都不需要把線程掛起橄妆,因此這種同步操作稱為非阻塞同步衙伶。
使用這種樂觀并發(fā)策略需要硬件的支持,需要將操作和沖突檢測(cè)這兩個(gè)步驟實(shí)現(xiàn)成原子性操作(硬件保證一個(gè)語(yǔ)義上看起來需要多次操作的行為只通過一條處理器指令就能完成)害碾。這類指令常用的有:
- 測(cè)試并設(shè)置(Test-and-Set)
- 獲取并增加(Fetch-and-Increment)
- 交換(Swap)
- 比較并交換(Compare-and-Swap矢劲,CAS)
- 加載鏈接/條件存儲(chǔ)(Load-Linked/Store-Conditional,LL/SC)
CAS指令需要3個(gè)操作數(shù):內(nèi)存位置V慌随、舊的預(yù)期值A(chǔ)芬沉、新值B。CAS指令執(zhí)行時(shí)阁猜,當(dāng)且僅當(dāng)V符合舊預(yù)期值A(chǔ)時(shí)丸逸,處理器用新值B更新V的值,否則不執(zhí)行更新剃袍,無(wú)論是否更新了V的值黄刚,都會(huì)返回V的舊值。
Java程序中的CAS操作由sun.misc.Unsafe類里面的compareAndSwapInt()民效、compareAndSwapLong()等方法提供憔维。但是這個(gè)Unsafe類不是提供給用戶程序調(diào)用的類,在J.U.C包里面的整數(shù)原子類畏邢,其中的compareAndSet()业扒、getAndIncrement()等方法使用了Unsafe類的CAS操作。
CAS在語(yǔ)義上存在一個(gè)邏輯漏洞:如果一個(gè)變量V初次讀取時(shí)是A值舒萎,并且在準(zhǔn)備賦值時(shí)檢查到它仍然是A值程储,并不能說明它的值沒有被其他線程改變過。如果在這段期間臂寝,它的值曾經(jīng)被改成了B虱肄,后來又改回為A,那CAS操作就會(huì)誤認(rèn)為它從來沒有被改變過交煞。這個(gè)漏洞稱為CAS操作的ABA問題咏窿。J.U.C包為了解決這個(gè)問題,提供了一個(gè)帶有標(biāo)記的原子引用類AtomicStampedReference素征,可以通過控制變量值的版本來保證CAS的正確性集嵌。
大部分情況下萝挤,ABA問題不會(huì)影響程序并發(fā)的正確性,如果需要解決ABA問題根欧,改用傳統(tǒng)的互斥同步可能會(huì)比原子類更高效怜珍。
2.3 無(wú)同步方案
要保證線程安全,并不是一定就要進(jìn)行同步凤粗,兩者沒有因果關(guān)系酥泛。同步只是保證共享數(shù)據(jù)爭(zhēng)用時(shí)的正確手段,如果一個(gè)方法本來就不涉及共享數(shù)據(jù)嫌拣,那它自然就無(wú)須任何同步措施去保證正確性柔袁,因此會(huì)有一些代碼天生就是線程安全的,如下面介紹的异逐。
-
可重入代碼
也叫做純代碼捶索,可以在代碼執(zhí)行的任何時(shí)刻中斷它,轉(zhuǎn)而去執(zhí)行另外一段代碼灰瞻,在控制權(quán)返回后腥例,原來的程序不會(huì)出現(xiàn)任何錯(cuò)誤。
可重入代碼有共同特征:如不依賴存儲(chǔ)在堆上的數(shù)據(jù)和公用的系統(tǒng)資源酝润、用到的狀態(tài)量都由參數(shù)中傳入燎竖、不調(diào)用非可重入的方法等。
線程本地存儲(chǔ)
3. 鎖優(yōu)化
3.1 鎖消除
鎖消除是指虛擬機(jī)即時(shí)編譯器在運(yùn)行時(shí)要销,對(duì)一些代碼上要求同步构回,但是被檢測(cè)到不可能存在共享數(shù)據(jù)競(jìng)爭(zhēng)的鎖進(jìn)行消除。
鎖消除的主要判定依據(jù)來源于逃逸分析的數(shù)據(jù)支持蕉陋。如果判斷在一段代碼中捐凭,堆上的所有數(shù)據(jù)都不會(huì)逃逸出去從而被其他線程訪問到,那就可以把它們當(dāng)做棧上數(shù)據(jù)對(duì)待凳鬓,認(rèn)為它們是線程私有的茁肠,同步加鎖自然無(wú)需進(jìn)行。
有許多同步措施不是程序員自己加入的缩举,而是由編譯器加入的垦梆。在即時(shí)編譯階段會(huì)將編譯器加入的確實(shí)無(wú)用的鎖消除。
3.2 鎖粗化
原則上仅孩,我們?cè)诰帉懘a時(shí)托猩,總是推薦將同步塊的作用范圍限制得盡量小——只在共享數(shù)據(jù)的實(shí)際作用域中才進(jìn)行同步,這樣是為了使得需要同步的操作數(shù)量盡可能變小辽慕,如果存在鎖競(jìng)爭(zhēng)京腥,那等待鎖的線程也能盡快拿到鎖。
大部分情況下溅蛉,上述原則是正確的公浪,但如果一系列的連續(xù)操作都對(duì)同一個(gè)對(duì)象反復(fù)加鎖他宛、解鎖,甚至加鎖操作出現(xiàn)在循環(huán)體中欠气,那即使沒有線程競(jìng)爭(zhēng)厅各,頻繁地進(jìn)行互斥同步操作也會(huì)導(dǎo)致不必要的性能損耗。
如果虛擬機(jī)探測(cè)到有這樣一串零碎的操作都對(duì)同一個(gè)對(duì)象加鎖预柒,將會(huì)把加鎖同步范圍擴(kuò)展(粗化)到整個(gè)操作序列的外部队塘。
3.3 偏向鎖
偏向鎖的目的是消除數(shù)據(jù)在無(wú)競(jìng)爭(zhēng)情況下的同步原語(yǔ),進(jìn)一步提高程序的運(yùn)行性能宜鸯。
偏向鎖是鎖會(huì)偏向于第一個(gè)獲得它的線程憔古,在接下來的執(zhí)行過程中,如果鎖沒有被其他的線程獲取顾翼,則持有偏向鎖的線程將永遠(yuǎn)不需要再進(jìn)行同步投放。
如果當(dāng)前虛擬機(jī)啟用了偏向鎖奈泪,那么當(dāng)鎖對(duì)象第一次被線程獲取時(shí)适贸,虛擬機(jī)將會(huì)把對(duì)象頭中的標(biāo)志位設(shè)為01。同時(shí)使用CAS操作把獲取到這個(gè)鎖的線程ID記錄在對(duì)象的Mark Word中涝桅,如果CAS成功拜姿,持有偏向鎖的線程以后每次進(jìn)入這個(gè)鎖相關(guān)的同步塊時(shí),虛擬機(jī)都可以不再進(jìn)行任何同步操作冯遂。
當(dāng)有另外一個(gè)線程嘗試獲得這個(gè)鎖時(shí)蕊肥,偏向模式宣告結(jié)束。根據(jù)鎖對(duì)象目前是否處于被鎖定的狀態(tài)蛤肌,撤銷偏向后恢復(fù)到未鎖定(01)或輕量級(jí)鎖定(00)的狀態(tài)壁却,后續(xù)的同步操作就如下面介紹的輕量級(jí)鎖那樣執(zhí)行。
偏向鎖可以提高帶有同步但無(wú)競(jìng)爭(zhēng)的程序性能裸准。
如果程序中大多數(shù)的鎖總是被多個(gè)不同的線程訪問展东,那偏向模式就是多余的。
3.4 輕量級(jí)鎖
輕量級(jí)鎖在無(wú)競(jìng)爭(zhēng)的情況下使用CAS操作消除同步使用的互斥量炒俱,偏向鎖是在無(wú)競(jìng)爭(zhēng)情況下把整個(gè)同步都消除掉盐肃,連CAS操作都不做了。
輕量級(jí)鎖是相對(duì)于使用操作系統(tǒng)的互斥量來實(shí)現(xiàn)的傳統(tǒng)鎖而言的权悟。
輕量級(jí)鎖并不是用來代替重量級(jí)鎖的砸王,它的本意是在沒有多線程競(jìng)爭(zhēng)的前提下,減少傳統(tǒng)的重量級(jí)鎖使用操作系統(tǒng)互斥量產(chǎn)生的性能損耗峦阁。
HotSpot虛擬機(jī)的對(duì)象頭(Object Header)分為兩部分信息:
- 一部分存儲(chǔ)對(duì)象自身的運(yùn)行時(shí)數(shù)據(jù)谦铃,如哈希碼、GC分代年齡等榔昔,這部分?jǐn)?shù)據(jù)的長(zhǎng)度在32位和64位的虛擬機(jī)中分別為32bit和64bit驹闰,官方稱之為Mark Word凿跳。這是實(shí)現(xiàn)輕量級(jí)鎖和偏向鎖的關(guān)鍵。
- 另一部分存儲(chǔ)指向方法區(qū)對(duì)象類型數(shù)據(jù)的指針疮方,如果是數(shù)組對(duì)象的話控嗜,還會(huì)有一個(gè)額外的部分用于存儲(chǔ)數(shù)組長(zhǎng)度。
HotSpot虛擬機(jī)對(duì)象頭Mark Word見下表:
存儲(chǔ)內(nèi)容 | 標(biāo)志位 | 狀態(tài) |
---|---|---|
對(duì)象哈希碼骡显、對(duì)象分代年齡 | 01 | 未鎖定 |
指向鎖記錄的指針 | 00 | 輕量級(jí)鎖定 |
指向重量級(jí)鎖的指針 | 10 | 膨脹(重量級(jí)鎖定) |
空疆栏,不需要記錄信息 | 11 | GC標(biāo)記 |
偏向線程ID、偏向時(shí)間戳惫谤、對(duì)象分代年齡 | 01 | 可偏向 |
輕量級(jí)鎖的執(zhí)行過程:
在代碼進(jìn)入同步塊時(shí)壁顶,如果此同步對(duì)象沒有被鎖定(01,鎖標(biāo)志位的狀態(tài))溜歪,虛擬機(jī)首先將在當(dāng)前線程的棧幀中建立一個(gè)名為鎖記錄(Lock Record)的空間若专,用于存儲(chǔ)鎖對(duì)象目前的Mark Word拷貝,官方稱為Displaced Mark Word蝴猪。此時(shí)線程堆棧與對(duì)象頭轉(zhuǎn)態(tài)如下圖:
然后调衰,虛擬機(jī)將使用CAS操作嘗試將對(duì)象的Mark Word更新為指向Lock Record的指針。如果這個(gè)動(dòng)作更新成功了自阱,那么這個(gè)線程就擁有了該對(duì)象的鎖嚎莉,并且對(duì)象Mark Word的鎖標(biāo)志位變?yōu)?0,表示此對(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ù)進(jìn)行叫确,否則說明這個(gè)鎖對(duì)象被其他線程搶占了。如果有兩條以上的線程爭(zhēng)用同一個(gè)鎖芍锦,那輕量級(jí)鎖就不再有效竹勉,要膨脹為重量級(jí)鎖,鎖標(biāo)志的狀態(tài)值變?yōu)?0醉旦,Mark Word中存儲(chǔ)的就是指向重量級(jí)鎖(互斥量)的指針饶米,后面等待鎖的線程也要進(jìn)入阻塞狀態(tài)。
輕量級(jí)鎖的解鎖過程也是通過CAS操作來進(jìn)行的车胡。如果對(duì)象的Mark Word仍然指向著線程的鎖記錄檬输,那就用CAS操作把對(duì)象當(dāng)前的Mark Word和線程中復(fù)制的Dislaced Mark Word替換回來,如果替換成功匈棘,整個(gè)同步過程就完成了丧慈。如果替換失敗,說明有其他線程嘗試過獲取該鎖,就要在釋放鎖的同時(shí)逃默,喚醒被掛起的線程鹃愤。
輕量級(jí)鎖能提升程序同步性能的依據(jù)是:對(duì)于大部分的鎖,在整個(gè)同步周期內(nèi)都是不存在競(jìng)爭(zhēng)的完域。這是一個(gè)經(jīng)驗(yàn)數(shù)據(jù)软吐。
如果沒有競(jìng)爭(zhēng),輕量級(jí)鎖使用CAS操作避免了使用互斥量的開銷吟税,但如果存在鎖競(jìng)爭(zhēng)凹耙,除了互斥量的開銷外,還額外發(fā)生了CAS操作肠仪,因此在有競(jìng)爭(zhēng)的情況下肖抱,輕量級(jí)鎖會(huì)比傳統(tǒng)的重量級(jí)鎖更慢。
3.5 自旋鎖與自適應(yīng)自旋
在許多應(yīng)用上异旧,共享數(shù)據(jù)的鎖定狀態(tài)只會(huì)持續(xù)很短的一端時(shí)間意述。可以讓后面請(qǐng)求鎖的線程“稍等一下”吮蛹,但不放棄處理器的執(zhí)行時(shí)間荤崇,看看持有鎖的線程是否很快就會(huì)釋放鎖。為了讓線程等待匹涮,只需讓線程執(zhí)行一個(gè)忙循環(huán)(自旋)天试,這項(xiàng)技術(shù)就是自旋鎖槐壳。
如果鎖被占用的時(shí)間很短然低,自旋等待的效果就會(huì)非常好。反之务唐,如果鎖被占用的時(shí)間很長(zhǎng)雳攘,那么自旋的線程只會(huì)白白消耗處理器資源,而不會(huì)做任何有用的工作枫笛,反而帶來性能上的浪費(fèi)吨灭。
自適應(yīng)自旋意味著自旋的時(shí)間不固定,由虛擬機(jī)自己根據(jù)監(jiān)控信息調(diào)整刑巧。