線程安全的實現(xiàn)方法
互斥同步
互斥是因耍休,同步是果;互斥是方法键痛,同步是目的炫彩。
synchronized關(guān)鍵字
-
synchronized
關(guān)鍵字是基本的互斥同步手段。它在編譯后會在同步代碼塊前后加入2條字節(jié)碼指令:monitorenter
和monitorexit
- 這兩個字節(jié)碼都需要一個
reference
類型的參數(shù)來指明要鎖定和解鎖的對象絮短。如果Java程序中的synchronized
指定了對象參數(shù)江兢,那就是這個對象的reference
;如果沒有指定丁频,就根據(jù)synchronized
修飾的是實例方法還是類方法杉允,去取對應(yīng)的對象實例或Class對象來作為鎖對象。 - 執(zhí)行
monitorenter
指令時席里,首先要嘗試獲取對象的鎖叔磷。如果這個對象沒被鎖定,或當(dāng)前線程已經(jīng)擁有了那個對象的鎖胁勺,把鎖的計數(shù)器加1;在執(zhí)行monitorexit
指令時會將鎖計數(shù)器減1独旷。當(dāng)計數(shù)器為0時署穗,鎖就被釋放寥裂。如果獲取對象鎖失敗,那當(dāng)前線程就要阻塞等待案疲,直到對象鎖被另外一個線程釋放為止封恰。 -
synchronized
同步塊對同一條線程來說是可重入的,不會出現(xiàn)自己把自己鎖死的問題褐啡。 - 同步塊在已進入的線程執(zhí)行完之前诺舔,會阻塞后面其他線程的進入。
- Java的線程是映射到操作系統(tǒng)的原生線程之上的备畦,如果要阻塞或喚醒一個線程低飒,都需要操作系統(tǒng)來完成,這就需要從用戶態(tài)轉(zhuǎn)換到核心態(tài)中懂盐,因此狀態(tài)轉(zhuǎn)換需要耗費很多的處理器時間褥赊,所以synchronized是Java語言中一個重量級的操作。不過虛擬機會有一些優(yōu)化措施莉恼,比如自旋等待拌喉。
ReentrantLock重入鎖
重入鎖位于java.util.concurrent
包±基本用法和synchronized
相似尿背,只是代碼寫法有區(qū)別:synchronized
是原生語法層面的實現(xiàn)。ReentrantLock
是API層面捶惜,使用lock()
和unlock()
方法配合try/finally
語句塊來實現(xiàn)田藐。
重入鎖有3個高級特性:
- 等待可中斷:當(dāng)持有鎖的線程長期不釋放鎖時,正在等待的線程可以選擇放棄等待售躁,改為處理其他事情坞淮。可中斷特性對處理執(zhí)行時間非常長的同步塊很有幫助陪捷。
- 可實現(xiàn)公平鎖:公平鎖是指多個線程在等待同一個鎖時回窘,必須按照申請鎖的時間順序來依次獲得鎖;而非公平鎖則不保證這一點市袖,在鎖被釋放時啡直,任何一個等待鎖的線程都有機會獲得鎖。
synchronized
中的鎖是非公平的苍碟,ReentrantLock
默認(rèn)情況下也是非公平的酒觅,但可以通過帶布爾值的構(gòu)造函數(shù)要求使用公平鎖。 - 鎖可以綁定多個條件:一個
ReentrantLock
對象可以同時綁定多個Condition
對象微峰,而在synchronized
中舷丹,鎖對象的wait()
和notify()
或notifyAll()
方法可以實現(xiàn)一個隱含的條件,如果要和多于一個的條件關(guān)聯(lián)的時候蜓肆,就不得不額外地添加一個鎖颜凯,而ReentrantLock
則無須這樣做谋币,只需要多次調(diào)用newCondition()
方法即可。
性能比較
- JDK1.6之前症概,在多線程環(huán)境下蕾额,
synchronized
的吞吐量隨著處理器數(shù)量增加而下降得非常嚴(yán)重。 - JDK1.6之后彼城,虛擬機做了優(yōu)化诅蝶,2種方式性能差不多。推薦優(yōu)先使用
synchronized
方式募壕。
非阻塞同步
互斥同步最主要的問題就是進行線程阻塞和喚醒所帶來的性能問題调炬,因此這種同步也稱為阻塞同步(Blocking Synchronization)。
按處理問題的方式來說:
- 互斥同步是悲觀并發(fā)策略:無論是否產(chǎn)生共享數(shù)據(jù)爭用司抱,都會做同步措施(加鎖筐眷,用戶態(tài)內(nèi)核態(tài)轉(zhuǎn)換等)。
- 非阻塞同步是一種樂觀并發(fā)策略:它基于沖突檢測习柠。通俗的說匀谣,就是先執(zhí)行代碼,若沒有發(fā)生共享數(shù)據(jù)爭用资溃,就成功執(zhí)行武翎;若發(fā)生共享數(shù)據(jù)爭用,就采取補償措施(比如不斷重試溶锭,直到成功)宝恶,這種策略不會導(dǎo)致線程阻塞。
CAS操作:
CAS指令需要有3個操作數(shù)趴捅,分別是內(nèi)存位置(在Java中可以簡單理解為變量的內(nèi)存地址垫毙,用V表示)、舊的預(yù)期值(用A表示)和新值(用B表示)拱绑。CAS指令執(zhí)行時综芥,當(dāng)且僅當(dāng)V符合舊預(yù)期值A(chǔ)時,處理器用新值B更新V的值猎拨,否則它就不執(zhí)行更新膀藐,但是無論是否更新了V的值,都會返回V的舊值红省,這個處理過程是個原子操作额各。
ABA問題:
如果一個變量V初次讀取的時候是A值,并且在準(zhǔn)備賦值的時候檢查到它仍然為A值吧恃,那我們就能說它的值沒有被其他線程改變過了嗎虾啦?如果在這段期間它的值曾經(jīng)被改成了B,后來又被改回為A,那CAS操作就會誤認(rèn)為它從來沒有被改變過傲醉。
無同步方案
如果一個方法本來就不涉及共享數(shù)據(jù)针饥,那它就無須任何同步措施去保證正確性。
- 可重入代碼:這種代碼也叫做純代碼(Pure Code)需频,可以在代碼執(zhí)行的任何時刻中斷它,轉(zhuǎn)而去執(zhí)行另外一段代碼(包括遞歸調(diào)用它本身)筷凤,而在控制權(quán)返回后昭殉,原來的程序不會出現(xiàn)任何錯誤。
- 線程本地存儲:一段代碼中所需要的數(shù)據(jù)必須與其他代碼共享藐守,并且可以把共享數(shù)據(jù)的可見范圍限制在同一個線程之內(nèi)挪丢,這樣,無須同步也能保證線程之間不出現(xiàn)數(shù)據(jù)爭用的問題卢厂。
- Java語言中乾蓬,如果一個變量要被多線程訪問,可以使用volatile關(guān)鍵字聲明它為“易變的”慎恒;如果一個變量要被某個線程獨享任内,Java中就沒有類似C++中__declspec(thread) 這樣的關(guān)鍵字,不過還是可以通過java.lang.ThreadLocal類來實現(xiàn)線程本地存儲的功能融柬。每一個線程的Thread對象中都有一個ThreadLocalMap對象死嗦,這個對象存儲了一組以ThreadLocal.threadLocalHashCode為鍵,以本地線程變量為值的K-V值對粒氧,ThreadLocal對象就是當(dāng)前線程的ThreadLocalMap的訪問入口越除,每一個ThreadLocal對象都包含了一個獨一無二的threadLocalHashCode值,使用這個值就可以在線程K-V值對中找回對應(yīng)的本地線程變量外盯。
鎖優(yōu)化
適應(yīng)性自旋(Adaptive Spinning)
線程阻塞的時候摘盆,讓等待的線程不放棄cpu執(zhí)行時間,而是執(zhí)行一個自旋(一般是空循環(huán))饱苟,這叫做自旋鎖孩擂。
自旋等待本身雖然避免了線程切換的開銷,但它是要占用處理器時間的掷空,因此肋殴,如果鎖被占用的時間很短,自旋等待的效果就非常好坦弟,反之护锤,如果鎖被占用的時間很長,那么自旋的線程只會白白消耗處理器資源酿傍,帶來性能上的浪費烙懦。
因此,自旋等待的時間必須要有一定的限度赤炒。如果自旋超過了限定的次數(shù)仍然沒有成功獲得鎖氯析,就應(yīng)當(dāng)使用傳統(tǒng)的方式去掛起線程了亏较。自旋次數(shù)的默認(rèn)值是10次,用戶可以使用參數(shù)-XX:PreBlockSpin
來更改掩缓。
JDK1.6引入了自適應(yīng)的自旋鎖雪情。自適應(yīng)意味著自旋的時間不再固定了,而是由前一次在同一個鎖上的自旋時間及鎖的擁有者的狀態(tài)來決定你辣。比如前一次自旋了3次就獲得了一個鎖巡通,那么下一次虛擬機會允許他自旋更多次來獲得這個鎖。如果一個鎖很少能通過自旋成功獲得舍哄,那么之后再遇到這個情況就會省略自旋過程了宴凉。
鎖消除(Lock Elimination)
虛擬機即時編譯器在運行時,對一些代碼上要求同步表悬,但是被檢測到不可能存在共享數(shù)據(jù)競爭的鎖進行消除弥锄。一般根據(jù)逃逸分析的數(shù)據(jù)支持來作為判定依據(jù)。
鎖粗化(Lock Coarsening)
原則上蟆沫,我們在編寫代碼的時候籽暇,總是推薦將同步塊的作用范圍限制得盡量小——只在共享數(shù)據(jù)的實際作用域中才進行同步,這樣是為了使需要同步的操作數(shù)量盡可能變小饭庞,如果存在鎖競爭图仓,那等待鎖的線程也能盡快拿到鎖。
但如果一系列操作頻繁對同一個對象加鎖解鎖但绕,或者加鎖操作再循環(huán)體內(nèi)救崔,會耗費性能,這時虛擬機會擴大加鎖范圍捏顺。
輕量級鎖(Lightweight Locking)
輕量級鎖是JDK 1.6之中加入的新型鎖機制六孵。它的作用是在沒有多線程競爭的前提下,減少傳統(tǒng)的重量級鎖使用操作系統(tǒng)互斥量產(chǎn)生的性能消耗幅骄。
HotSpot虛擬機的對象頭(Object Header)分為兩部分信息劫窒,第一部分用于存儲對象自身的運行時數(shù)據(jù),這部分稱為Mark Word
拆座。還有一部分存儲指向方法區(qū)對象類型數(shù)據(jù)的指針主巍。
加鎖
在代碼進入同步塊的時候,如果此同步對象沒有被鎖定(鎖標(biāo)志位為“01”狀態(tài))挪凑,虛擬機首先將在當(dāng)前線程的棧幀中建立一個名為鎖記錄(Lock Record)的空間孕索,用于存儲鎖對象目前的Mark Word的拷貝(官方把這份拷貝加了一個Displaced前綴,即Displaced Mark Word)躏碳。然后搞旭,虛擬機將使用CAS操作嘗試將對象的Mark Word更新為指向Lock Record的指針。如果這個更新動作成功,那么這個線程就擁有了該對象的鎖肄渗,并且對象Mark Word的鎖標(biāo)志位(Mark Word的最后2bit)將轉(zhuǎn)變?yōu)椤?0”镇眷,即表示此對象處于輕量級鎖定狀態(tài)。如果這個更新操作失敗了翎嫡,虛擬機首先會檢查對象的Mark Word是否指向當(dāng)前線程的棧幀欠动,如果是說明當(dāng)前線程已經(jīng)擁有了這個對象的鎖,那就可以直接進入同步塊繼續(xù)執(zhí)行惑申,否則說明這個鎖對象已經(jīng)被其他線程搶占了翁垂。如果有兩條以上的線程爭用同一個鎖,那輕量級鎖就不再有效硝桩,要膨脹為重量級鎖,鎖標(biāo)志的狀態(tài)值變?yōu)椤?0”枚荣,Mark Word中存儲的就是指向重量級鎖(互斥量)的指針碗脊,后面等待鎖的線程也要進入阻塞狀態(tài)。
解鎖
解鎖過程也是通過CAS操作來進行的橄妆。如果對象的Mark Word仍然指向著線程的鎖記錄衙伶,那就用CAS操作把對象當(dāng)前的Mark Word和線程中復(fù)制的Displaced Mark Word替換回來,如果替換成功害碾,整個同步過程就完成了矢劲。如果替換失敗,說明有其他線程嘗試過獲取該鎖慌随,那就要在釋放鎖的同時芬沉,喚醒被掛起的線程。
性能
沒有鎖競爭時阁猜,輕量級鎖用CAS操作替代互斥量的開銷丸逸,性能較優(yōu)。有鎖競爭時剃袍,除了互斥量開銷黄刚,還有CAS操作開銷,所以性能較差民效。但是憔维,一般情況下,在整個同步周期內(nèi)都是不存在競爭的”畏邢,這是一個經(jīng)驗數(shù)據(jù)业扒。
偏向鎖(Biased Locking)
偏向鎖也是JDK1.6中引入的鎖優(yōu)化,它的目的是消除數(shù)據(jù)在無競爭情況下的同步原語舒萎,進一步提高程序的運行性能凶赁。如果說輕量級鎖是在無競爭的情況下使用CAS操作去消除同步使用的互斥量,那偏向鎖就是在無競爭的情況下把整個同步都消除掉,連CAS操作都不做了虱肄。
當(dāng)鎖對象第一次被線程獲取的時候致板,虛擬機將會把對象頭中的標(biāo)志位設(shè)為“01”,即偏向模式咏窿。同時使用CAS操作把獲取到這個鎖的線程的ID記錄在對象的Mark Word之中斟或,如果CAS操作成功,持有偏向鎖的線程以后每次進入這個鎖相關(guān)的同步塊時集嵌,虛擬機都可以不再進行任何同步操作萝挤。當(dāng)有另外一個線程去嘗試獲取這個鎖時,偏向模式結(jié)束根欧。
偏向鎖可以提高帶有同步但無競爭的程序性能怜珍,但并不一定總是對程序運行有利。如果程序中大多數(shù)的鎖總是被多個不同的線程訪問凤粗,那偏向模式就是多余的酥泛。在具體問題具體分析的前提下,有時候使用參數(shù)-XX:-UseBiasedLocking
來禁止偏向鎖優(yōu)化反而可以提升性能嫌拣。