一肆饶、線程安全的實現(xiàn)方法
(一)互斥同步
- 互斥是實現(xiàn)同步的一種手段,臨界區(qū)(Critical Section)岖常、互斥量(Mutex)驯镊、信號量(Semaphore)都是主要的互斥實現(xiàn)方式。
互斥量和信號量在系統(tǒng)中的任何進程都是可見的,臨界區(qū)的作用范圍僅限于本進程阿宅。
- java中,最基本的互斥同步手段就是
synchronized
關(guān)鍵字笼蛛,該關(guān)鍵字經(jīng)過編譯之后洒放,會在同步塊的前后分別形成monitorenter
和monitorexit
這兩個字節(jié)碼指令,這兩個字節(jié)碼都需要一個reference
類型的參數(shù)來指明要鎖定和解鎖的對象滨砍。 - 根據(jù)虛擬機規(guī)范的要求往湿,在執(zhí)行
monitorenter
指令時狭魂,首先要嘗試獲取對象的鎖榆浓。如果這個對象沒被鎖定漏益,或者當(dāng)前線程已經(jīng)擁有了那個對象的鎖砂心,則把鎖的計數(shù)器加1叠穆,相應(yīng)的在執(zhí)行monitorexit
指令時將鎖計數(shù)器減1此疹,當(dāng)計數(shù)器為0時末誓,鎖就被釋放拷恨。如果獲取對象鎖失敗舔亭,那當(dāng)前線程就要阻塞等待些膨,直到對象鎖被另一個線程釋放位置。package tystudy.javabasic.jvm; public class MonitorTest { public static void main(String[] args) { final Object lock = new Object(); synchronized(lock) { System.out.println("hello"); } } }
E:\myworkspace\my-study\common-project\java-basic\target\classes>javap -c tystudy.javabasic.jvm.MonitorTest Compiled from "MonitorTest.java" public class tystudy.javabasic.jvm.MonitorTest { public tystudy.javabasic.jvm.MonitorTest(); Code: 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: return public static void main(java.lang.String[]); Code: 0: new #2 // class java/lang/Object 3: dup 4: invokespecial #1 // Method java/lang/Object."<init>":()V 7: astore_1 8: aload_1 9: dup 10: astore_2 11: monitorenter 12: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream; 15: ldc #4 // String hello 17: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 20: aload_2 21: monitorexit 22: goto 30 25: astore_3 26: aload_2 27: monitorexit 28: aload_3 29: athrow 30: return Exception table: from to target type 12 22 25 any 25 28 25 any }
- java的線程是映射到操作系統(tǒng)的原生線程之上的钦铺,如果要阻塞或喚醒一個線程订雾,都需要操作系統(tǒng)來幫忙完成,這就需要從用戶態(tài)轉(zhuǎn)換到核心態(tài)(內(nèi)核態(tài))中矛洞,因此狀態(tài)轉(zhuǎn)換需要耗費很多的處理器時間洼哎。
- 內(nèi)核態(tài):控制計算機的硬件資源,并提供上層應(yīng)用程序的運行環(huán)境沼本。
- 用戶態(tài):上層應(yīng)用程序的活動空間噩峦,應(yīng)用程序的執(zhí)行必須依托于內(nèi)核態(tài)提供的資源。
- 系統(tǒng)調(diào)用:為了使上層應(yīng)用能夠訪問到這些資源抽兆,內(nèi)核為上層應(yīng)用提供訪問的接口壕探。
- 對于代碼簡單的同步塊,狀態(tài)轉(zhuǎn)換消耗的時間可能比用戶代碼執(zhí)行的時間還長郊丛。所以
synchronized
是java語言中一個重量級的操作李请。虛擬機本身也會進行一些優(yōu)化,比如在通知操作系統(tǒng)阻塞線程之前加入一段自旋等待過程厉熟,避免頻繁地切入到核心態(tài)之中导盅。 - 除了
synchronized
還可以使用ReentrantLock
實現(xiàn)同步,它是表現(xiàn)為api層面的互斥鎖揍瑟。相比synchronized
增加了幾個高級功能:等待可中斷白翻、公平鎖、綁定條件。jdk1.6之前ReentrantLock
性能更優(yōu)滤馍,jdk1.6后對synchronized
進行了優(yōu)化岛琼,性能與ReentrantLock
差不多。
(二)非阻塞同步
隨著硬件指令集的發(fā)展巢株,我們有了另外一個選擇:基于沖突檢測的樂觀并發(fā)策略槐瑞。通俗的說,就是先進行操作阁苞,如果沒有其他線程爭用共享數(shù)據(jù)困檩,那操作就成功了,如果共享數(shù)據(jù)有爭用那槽,產(chǎn)生了沖突悼沿,那就再采取其他的補償措施(最常見的補償措施就是不斷重試,直到成功為止)骚灸,這種樂觀的并發(fā)策略的許多實現(xiàn)都不需要把線程掛起糟趾,因此這種同步操作被稱為非阻塞同步(non-blocking synchronization)。
-
硬件保證一個從語義看起來需要多次操作的行為通過一條處理器指令就能完成甚牲,這類指令常用的有:
- 測試并設(shè)置(test-and-set)
- 獲取并增加(fetch-and-increment)
- 交換(swap)
- 比較并交換(compare-and-swap拉讯,cas)
- 加載鏈接/條件存儲(load-linked/store-conditional,ll/sc)
cas指令需要有3個操作數(shù)鳖藕,分別是內(nèi)存位置(V)魔慷、預(yù)期值(A)和新值(B)。cas指令執(zhí)行時著恩,當(dāng)且僅當(dāng)V符合舊預(yù)期值A(chǔ)時院尔,處理器用新值B更新V的值,否則它就不執(zhí)行更新喉誊,但是無論是否更新了V的值邀摆,都會返回V的舊值,上述處理過程是一個原子操作伍茄。
jdk1.5之后栋盹,java程序中才可以使用cas操作,該操作由
sun.misc.Unsafe
類里面的compareAndSwapInt()
和compareAndSwapLong()
等幾個方法包裝提供敷矫,虛擬機在內(nèi)部對這些方法做了特殊處理例获,即時編譯出來的結(jié)果就是一條平臺相關(guān)的處理器cas指令,沒有方法調(diào)用的過程曹仗,或者可以認(rèn)為是無條件內(nèi)聯(lián)進去了榨汤。cas存在ABA問題,juc為了解決這個問題提供了一個帶有標(biāo)記的原子引用類
AtomicStampedReference
怎茫,它可以通過控制變量值的版本來保證cas的正確性收壕,如果要解決ABA問題,改用傳統(tǒng)的互斥同步可能會比原子類更高效。
二蜜宪、鎖優(yōu)化
(一)自旋鎖與自適應(yīng)自旋
- 掛起和恢復(fù)線程的操作都需要轉(zhuǎn)入內(nèi)核態(tài)完成虫埂,這些操作給系統(tǒng)的并發(fā)性帶來很大的壓力。所以可以讓后面請求鎖的那個線程忙循環(huán)(自旋)等待圃验,每次自旋一次就查看持有鎖的線程是否已經(jīng)釋放鎖掉伏。而不需要進入內(nèi)核態(tài)掛起線程。
- 自旋鎖在jdk1.4.2中就已經(jīng)引入损谦,不過默認(rèn)是關(guān)閉的岖免,可以通過
-XX:+UseSpinning
參數(shù)來開啟岳颇,==jdk1.6中默認(rèn)開啟==照捡。 - 自旋等待本身雖然避免了線程切換的開銷,但它是要占用處理器時間的话侧,因此栗精,如果鎖被占用的時間很短,自旋等待的效果就會非常好瞻鹏,反之悲立,如果鎖被占用的時間很長,那么自旋的線程只會白白消耗處理器資源新博,而不會做任何有用的工作薪夕,反而會帶來性能上的浪費。所以自旋超過指定次數(shù)仍然沒有獲取鎖應(yīng)該使用傳統(tǒng)方式掛起線程赫悄。==自旋次數(shù)的默認(rèn)值是10次原献,用戶可以使用參數(shù)
-XX:PreBlockSpin
來更改。== - jdk1.6中引入了自適應(yīng)的自旋鎖,即對于經(jīng)常很快就可以獲取鎖的情況會多自旋一會,對于很少能夠通過自旋獲取鎖的就盡早或直接進入內(nèi)核態(tài)掛起線程埂淮。
(二)鎖消除
虛擬機即時編譯器在運行時會對一些代碼上要求同步姑隅,但是會對被檢測到不可能存在共享數(shù)據(jù)競爭的鎖進行消除。鎖消除主要判斷依據(jù)來源于逃逸分析的數(shù)據(jù)支持倔撞。如果判斷在一段代碼中讲仰,堆上的所有數(shù)據(jù)都不會逃逸出去被其他線程訪問到,那么就可以把它當(dāng)做棧上數(shù)據(jù)來對待痪蝇,認(rèn)為它們是線程私有的鄙陡,同步加鎖自然就無須進行。
-
我們也知道躏啰,對于String是一個不可變類柔吼,對字符串的連接操作總是通過生成新的String對象來進行的,因此Javac編譯器會對String連接做自動優(yōu)化丙唧。在jdk1.5之前愈魏,會轉(zhuǎn)化為StringBuffer對象的連續(xù)append操作,在jdk1.5之后會轉(zhuǎn)化為StringBuilder對象的連續(xù)append。對于StringBuffer的連續(xù)append培漏,這個方法是同步的溪厘,鎖就是this即StringBuffer對象。虛擬機會觀察這個鎖牌柄,發(fā)現(xiàn)它的攻臺作用域被限制在concatString方法內(nèi)部畸悬。也就是說,鎖對象的所有引用永遠(yuǎn)不會“逃逸”到concatString方法之外珊佣,其他線程無法訪問到它蹋宦,因此,雖然這里有鎖咒锻,但是可以被安全地消除掉冷冗,在即時編譯后,這段代碼就會忽略掉所有的同步而直接執(zhí)行了惑艇。
public String concatString(String s1, String s2, String s3) { return s1 + s2 + s3; }
(三)鎖粗化
- 原則上蒿辙,我們編寫代碼的時候,總是推薦將同步塊的作用范圍限制得盡量小滨巴。但是對于一系列的連續(xù)操作都是對同一對象反復(fù)加鎖和解鎖思灌,甚至加鎖操作是出現(xiàn)在循環(huán)體中的,那即使沒有線程競爭恭取,頻繁地進行互斥同步操作也會導(dǎo)致不必要的性能消耗泰偿。
-
StringBuffer
的連續(xù)append
方法就屬于這類情況。如果虛擬機探測到有這樣一串零碎的操作都對同一對象加鎖蜈垮,將會把加鎖同步的范圍擴展(粗化)到整個操作序列的外部耗跛。
(四)輕量級鎖
- 輕量級鎖是相對于使用操作系統(tǒng)互斥量來實現(xiàn)的傳統(tǒng)鎖而言的。
- 對象頭分為三個部分:
-
mark word
:hashcode窃款、gc分代年齡等信息课兄、指向鎖記錄的指針、指向重量級鎖的指針晨继、偏向線程id烟阐、偏向時間戳等 - 指向方法區(qū)對象類型數(shù)據(jù)的指針
- 如果是數(shù)組,這里會存儲數(shù)組長度
-
- 輕量級鎖能提升程序同步性能的依據(jù)是“對于絕大部分的鎖紊扬,在整個同步周期內(nèi)都是不存在競爭的”蜒茄,這是一個經(jīng)驗數(shù)據(jù)。如果沒有競爭餐屎,輕量級鎖使用cas操作避免了使用互斥量的開銷檀葛,如果存在鎖競爭,除了互斥量的開銷外腹缩,還額外發(fā)生了cas操作屿聋,因此在有競爭的情況下空扎,輕量級鎖會比傳統(tǒng)的重量級鎖更慢。
1润讥、輕量級鎖的加鎖過程:
在代碼進入同步塊的時候转锈,如果此同步對象沒有被鎖定(鎖標(biāo)識為01狀態(tài)),虛擬機首先將在當(dāng)前線程的棧幀中建立一個名為鎖記錄(Lock Record
)的空間楚殿,用于存儲鎖對象目前的Mark Word
的拷貝(官方把這個拷貝叫Displaced Mark Word
)撮慨。然后虛擬機將使用cas操作嘗試將對象的mark word
更新為指向Lock Record
的指針。
如果這個更新動作成功了脆粥,那么這個線程就擁有了該對象的鎖砌溺,并且對象Mark Word
的鎖標(biāo)志位將轉(zhuǎn)變?yōu)?code>00,即表示此對象處于輕量級鎖定狀態(tài)变隔。
如果這個更新動作失敗了规伐,虛擬機首先會檢查對象Mark Word
是否指向當(dāng)前線程的棧幀,如果只說明當(dāng)前線程已經(jīng)擁有了這個對象的鎖弟胀,那就可以直接進入同步塊繼續(xù)執(zhí)行楷力,否則說明這個鎖對象已經(jīng)被其他線程搶占了喊式。
如果有兩條以上的線程爭用同一個鎖孵户,那輕量級鎖就不再有效,要膨脹為重量級鎖岔留,鎖標(biāo)志位的狀態(tài)變?yōu)?code>10夏哭,Mark Word
中存儲的就是指向重量級鎖(互斥量)的指針,后面等待鎖的線程也要進入阻塞狀態(tài)献联。
2竖配、輕量級鎖的解鎖過程
解鎖過程也是通過cas操作來進行的,如果對象的Mark Word
仍然指向線程的鎖記錄里逆,那就用cas操作把對象當(dāng)前的Mark Word
和線程中復(fù)制的Displaced Mark Word
替換回來进胯,如果替換成功了,整個同步過程就完成了原押。如果替換失敗胁镐,說明有其他線程嘗試過獲取該鎖,那就要在釋放鎖的同時诸衔,喚醒被掛起的線程盯漂。
(五)偏向鎖
- 偏向鎖也是jdk1.6中引入的一項鎖優(yōu)化,它的目的是消除數(shù)據(jù)在無競爭情況下的同步原語笨农,進一步提高程序的運行性能就缆。如果說輕量級鎖是在無競爭的情況下使用cas操作去消除同步使用的互斥量,那偏向鎖就是在無競爭的情況下把整個同步都消除掉谒亦,連cas操作都不做了竭宰。
1空郊、偏向鎖原理
假設(shè)當(dāng)前虛擬機啟用了偏向鎖(==-XX:+UseBiasedLocking
,這是jdk1.6的默認(rèn)值==),那么當(dāng)鎖對象第一次被線程獲取的時候切揭,虛擬機將會把對象頭中的標(biāo)志位設(shè)為01渣淳,即偏向模式。同時使用cas操作把獲取到這個鎖的線程的id記錄在對象Mark Word
之中伴箩,如果cas操作成功入愧,持有偏向鎖的線程以后每次進入這個鎖相關(guān)的同步塊時,虛擬機都可以不再進行任何同步操作(例如Locking
嗤谚、Unlocking
及對Mark Word
的Update
等)棺蛛。
當(dāng)有另外一個線程去嘗試獲取這個鎖時,偏向模式就宣告結(jié)束巩步。根據(jù)鎖對象目前是否處于被鎖定的狀態(tài)旁赊,撤銷偏向后恢復(fù)到未鎖定(標(biāo)志位為01
)或輕量級鎖(標(biāo)志位為00
)的狀態(tài)。后續(xù)的同步操作就如上面介紹的輕量級鎖那樣執(zhí)行椅野。
如果程序中大多數(shù)的鎖總是被多個不同的線程訪問终畅,那偏向模式就是多余的,使用-XX:-UseBiasedLocking
來禁止偏向鎖優(yōu)化反而可以提升性能竟闪。