jvm(13)-线程安全与锁优化
【0】README
0.1)本文部分文字轉自“深入理解jvm”, 旨在學習?線程安全與鎖優化?的基礎知識;
0.2)本文知識對于理解 java并發編程非常有用,個人覺得,所以我總結的很詳細;
【1】概述
【2】線程安全
1)線程安全定義:當多個線程訪問一個對象時,如果不用考慮這些線程在運行時環境下的調度和交替執行,也不需要進行額外的同步,或者在調用方進行任何其他的協調操作,調用這個對象的行為都可以獲得正確的結果,那這個對象是線程安全的;(干貨——線程安全定義)?
【2.1】java 語言中的線程安全(干貨——java中各種操作共享的數據分為以下5類) 0)java中各種操作共享的數據分為以下5類:不可變, 絕對線程安全, 相對線程安全,線程兼容,線程對立; 1)不可變對象:該對象一定是線程安全的,無論是對象的方法實現還是方法的調用者,都不需要采取任何的線程安全保障措施; 1.1)如果共享數據是一個基本數據類型,那么只要在定義時使用 final 關鍵字修飾它就可以保證它是不可變的; 1.2)不妨想想java.lang.String類的對象:它是一個典型的不可變對象,調用它的substring(), replace(), concat() 這些方法都不會影響它原來的值,只會返回一個新構造的字符串對象; 1.3)看個荔枝:如java.lang.Integer 構造函數所示的,將value定義為final 來保障狀態不變;?
2)絕對線程安全
2.1)在java API中標注自己是線程安全的類,大多數都不是絕對的線程安全
2.2)java.util.Vector 是一個線程安全的容器,因為它的add()方法,get()方法,size() 方法 這些方法都是被 synchronized修飾的,盡管效率低下,但確實是安全的;對Vector的測試如下:?
// 對線程安全的容器 Vector的測試 public class VectorTest {private static Vector<Integer> vector = new Vector<>(); public static void main(String[] args) {while(true) {for (int i = 0; i < 100; i++) {vector.add(i);}Thread removeThread = new Thread(new Runnable() {@Overridepublic void run() {for (int i = 0; i < vector.size(); i++) {vector.remove(i);}}});Thread printThread = new Thread(new Runnable() {@Overridepublic void run() {for (int i = 0; i < vector.size(); i++) {System.out.println(vector.get(i));}}});removeThread.start();printThread.start();// 不要同時產生過多的線程,否則會導致os 假死while(Thread.activeCount() > 20);}} }?對以上代碼的分析(Analysis):
A1)運行結果:?作者說會拋出異常(但我的運行結果卻沒有拋出異常),按理說應該是會拋出異常的;
A2)拋出異常的原因:因為如果另一個線程恰好在錯誤的時間里刪除了一個元素,導致序號i 已經不再可用的話,再用i 訪問數組就會拋出一個 ArrayIndexOutOfBoundsException。
A3)如果要保證這段代碼能夠正確執行下去,修改后的代碼為:
-  - // 對線程安全的容器 Vector的測試(修改后的代碼) public class ModifiedVectorTest {private static Vector<Integer> vector = new Vector<>(); public static void main(String[] args) {while(true) {for (int i = 0; i < 100; i++) {vector.add(i);}Thread removeThread = new Thread(new Runnable() {@Overridepublic void run() {synchronized (vector) { // 添加同步塊,this linefor (int i = 0; i < vector.size(); i++) {vector.remove(i);}}}});Thread printThread = new Thread(new Runnable() {@Overridepublic void run() {synchronized (vector) { // 添加同步塊,this linefor (int i = 0; i < vector.size(); i++) {System.out.println(vector.get(i));}}}});removeThread.start();printThread.start();// 不要同時產生過多的線程,否則會導致os 假死while(Thread.activeCount() > 20);}} }
 
3.1)上述 VectorTest.java 和 ModifiedVectorTest.java 就是相對線程安全的案例;
4)線程兼容4.1)線程兼容定義:線程兼容是指對象本身并不是線程安全的,但是可以通過在調用端正確地使用同步手段來保證對象在并發環境中可以安全地使用;
5)線程對立 5.1)線程對立定義:指無論調用端是否采取了同步措施,都無法在多線程環境中并發使用的代碼; 5.2)由于java語言天生就具備多線程特性,線程對立這種排斥多線程的代碼是很少出現的,而且通常是有害的,應當盡量避免; 5.3)線程對立的荔枝:Thread類的suspend() 和 resume() 方法;如果有兩個線程同時持有一個線程對象,一個嘗試去中斷線程,另一個嘗試去恢復線程,如果并發進行的話,無論調用時是否進行了同步,目標線程都是存在死鎖風險的。正由于這個原因,suspend和result方法已經被JDK廢棄了了(@Deprecated)【2.2】線程安全的實現方法 1)互斥同步 1.1)互斥同步:是常見的并發正確性保障手段; 1.2)同步:是指在多個線程并發訪問共享數據時,保證共享數據在同一個時刻被一個線程使用。 1.3)互斥:互斥是實現同步的一種手段;臨界區,互斥量和信號量都是主要的互斥實現方式。因此,在這4個字里面,互斥是因,同步是果;互斥是方法,同步是目的; 1.4)最基本的互斥同步手段就是 synchronized關鍵字:synchronized關鍵字經過 編譯之后,會在同步塊的前后分別形成 monitorenter 和 monitorexit 這個兩個字節碼指令,這兩個字節碼都需要一個 reference類型的參數來指明要鎖定和解鎖的對象;如果java程序中的synchronized明確指定了對象參數,那就是這個對象的reference;如果沒有明確指定,那就根據 synchronized修飾的實例方法還是類方法,去取對應的對象實例或Class 對象來作為鎖對象;(干貨——最基本的互斥同步手段就是 synchronized關鍵字) 1.5)根據虛擬機規范的要求:在執行monitorenter指令時,如果這個對象沒有鎖定或當前線程已經擁有了那個對象的鎖,鎖的計數器加1,相應的,在執行 monitorexit 指令時會將鎖計數器減1;當計數器為0時,鎖就被釋放了;(干貨——執行monitorenter和monitorexit 指令) Attention)對于monitorenter 和 monitorexit 的行為描述中,有兩點需要注意: A1)synchronized同步塊對同一條線程來說是可重入的, 不會出現自己把自己鎖死的問題; A2)同步塊在已進入的線程執行完之前,會阻塞后面其他線程 的進入; 1.6)除了synchronized之外,還可以使用 java.util.concurrent 包中的重入鎖(ReentrantLock)來實現同步;(干貨——引入重入鎖進行同步) 1.6.1)synchronized 和 ReentrantLock 的區別:?一個表現為 API 層面的互斥鎖(lock() 和 unlock() 方法配合 try/finally 語句塊來完成),另一個表現為 原生語法層面的互斥鎖; 1.6.2)ReentrantLock增加了一些高級功能:主要有3項:等待可中斷,可實現公平鎖, 以及鎖可以綁定多個條件;(干貨——ReentrantLock 增加了3項高級功能) case1)等待可中斷:指當持有鎖的線程長期不釋放鎖的時候,正在等待的線程可以選擇放棄等待,改為處理其他事情,可中斷特性對處理執行時間非常長的同步塊很有幫助; case2)公平鎖:指多個線程在等待同一個鎖時,必須按照申請鎖的時間順序來依次獲得鎖; case3)鎖綁定多個條件:指一個 ReentrantLock對象可以同時綁定多個 Condition對象,而在 synchronized中,鎖對象的wait() 和 notify() 或 notifyAll() 方法可以實現一個隱含的條件,如果要和多于一個的條件關聯的時候,就不得不額外地添加一個鎖,而ReentrantLock 則無需這樣做,只需要多次調用 newCondition() 方法即可;(干貨——可重入鎖ReentrantLock 和 synchronized 綁定多個條件的實現方式的區別)
1.6.3)關于synchronized 和 ReentrantLock 性能的分析:
對上圖的分析(Analysis):
A1)多線程環境下 synchronized的吞吐量下降得非常嚴重,而 ReentrantLock 則能基本保持在同一個比較穩定的水平上;與其說ReentrantLock性能好,還不如說 synchronized還有非常大的優化余地;
A2)虛擬機在未來的性能改進中肯定也會更加偏向于原生的 synchronized,所以還是提倡在 synchronized能實現需求的情況下,優先考慮使用 synchronized 來進行同步;(干貨——同步方式推薦使用synchronized)
2)非阻塞同步 2.1)阻塞同步(互斥同步)的問題:就是進行線程阻塞和喚醒所帶來的性能問題,互斥同步屬于一種悲觀的并發策略,無論共享數據是否真的會出現競爭,它都要進行加鎖,用戶態核心態轉換,維護鎖計數器和檢查是否有被阻塞的線程需要喚醒等操作;(干貨——阻塞同步(互斥同步)的問題) 2.2)非阻塞同步定義:基于沖突檢測的樂觀并發策略,通俗的說,就是先進行操作,如果沒有其他線程爭用共享數據,那操作就成功了;如果共享數據有爭用,產生了沖突,那就再采用其他的補償措施,這種樂觀的并發策略的許多實現都不需要把線程掛起,因此這種同步操作稱為?非阻塞同步;(干貨——非阻塞同步定義) 2.3)為什么作者要說使用樂觀并發策略需要“硬件指令集的發展”才能進行呢?因為 我們需要操作和沖突檢測這兩個步驟具備原子性,靠什么來保證呢? 2.3.1)硬件:保證一個從語義上看起來需要多次操作的行為只通過一次處理器指令就能完成,這類指令常用的有:(instructions) i1)測試并設置(Test-and-Set); i2)獲取并增加(Fetch-and-Increment); i3)交換(Swap); i4)比較并交換(Compare-and-Swap,下文簡稱 CAS); i5)加載鏈接/ 條件存儲(Load-Linked/Store-Conditional,下文簡稱 LL/SC);2.4)如何使用CAS 操作來避免阻塞同步,看個荔枝:(測試incrementAndGet 方法的原子性)
// Atomic 變量自增運算測試(incrementAndGet 方法的原子性) public class AtomicTest {public static AtomicInteger race = new AtomicInteger(0);public static void increase() {// 輸出正確結果,一切都要歸功于 incrementAndGet 方法的原子性 race.incrementAndGet(); }public static final int THREADS_COUNT = 20;public static void main(String[] args) throws Exception {Thread[] threads = new Thread[THREADS_COUNT];for (int i = 0; i < threads.length; i++) {threads[i] = new Thread(new Runnable() {@Overridepublic void run() {for (int j = 0; j < 10000; j++) {increase();}}});threads[i].start();}while(Thread.activeCount() > 1) {Thread.yield();}System.out.println(race);}/*** incrementAndGet() 方法的JDK 源碼* Atomically increment by one the current value.* @return the updated value*/public final int incrementAndGet() {for(;;) {int current = get();int next = current + 1;if(compareAndSet(current,next)) {return next;}}} }2.5)CAS操作(比較并交換操作)的ABA問題:如果一個變量V初次讀取的時候是A值,并且在準備賦值的時候檢查到它仍然是A值,那我們就說它的值沒有被其他線程改變過了嗎? 如果在這段期間它的值曾經被改為了B,之后又改回了A,那CAS操作就會誤認為它從來沒有被改變過,這個漏洞稱為 CAS操作的 ABA問題;
2.6)解決方法:J.U.C 包為了解決這個問題,提供了一個帶有標記的原子引用類“AtomicStampedReference”,它可以通過控制變量值的version 來保證CAS的正確性。不過目前來說這個類比較雞肋, 大部分cases 下 ABA問題 不會影響程序并發的正確性,如果需要解決ABA問題,改用傳統的互斥同步可能會比原子類更高效;(干貨——CAS操作(比較并交換操作)的ABA問題及其解決方法)?
3)無同步方案 3.0)intro:?如果一個方法本來就不涉及共享數據,那它自然就無須任何同步措施去保證正確性,因此會有一些代碼天生就是線程安全的;下面介紹兩類線程安全代碼: 3.1)第一類線程安全代碼——可重入代碼:也叫作純代碼,可以在代碼執行的任何時刻中斷它,轉而去執行另外一段代碼,而在控制權返回后,原來的程序不會出現任何錯誤;?(干貨——可重入代碼定義) 3.1.1)所有的可重入代碼都是線程安全的; 3.1.2)如何判斷代碼是否具備可重入性:如果一個方法,它的返回結果是可以預測的,只要輸入了相同的數據,就都能返回相同的結果,那它就滿足可重入性的要求,當然也就是線程安全的; 3.2)第二類線程安全代碼——線程本地存儲:如果一段代碼中所需要的數據必須與其他代碼共享,那就看看這些共享數據的代碼是否能夠保證在同一線程中執行? 如果能保證,我們就可以把共享數據的可見范圍限制在同一個線程內,這樣,無需同步也可以保證線程間不出現數據爭用問題;【3】鎖優化 【3.1】 自旋鎖與自適應自旋(干貨——引入自旋鎖與自適應自旋) 1)problem:前文中我們提到,互斥同步對性能最大的影響是阻塞的實現,掛起線程和恢復線程的操作都需要轉入內核態中完成,共享數據的鎖定狀態只會持續很短的一段時間,為了這段時間去掛起和恢復線程很不值得;(干貨——產生自旋鎖與自適應自旋的背景) 2)自旋鎖定義:為了讓線程等待,我們只需讓線程執行一個忙循環(自旋),這項技術就是所謂的自旋鎖;(solution) 2.1)jdk1.6中 自旋鎖是默認開啟的,可以使用 -XX:+UseSpinning 參數來開啟; 2.2)自旋等待的時間必須要有一定的限度:?如果自旋超過了限定的次數仍然沒有成功獲得鎖,就應當使用傳統的方式去掛起線程了。自旋次數的默認值是10,用戶可以用參數 -XX:PreBlockSpin 來更改; 2.3)自適應自旋鎖:jdk1.6 中引入了自適應的自旋鎖。自適應意味著自旋的時間不再固定了,而是由前一次在同一個鎖上的自旋時間及鎖的擁有者的狀態來決定; case1)如果在同一個鎖對象上,自旋等待剛剛成功獲得過鎖,并且持有鎖的線程正在運行中,那么虛擬機就會認為這次自旋也很有可能再次成功,進而它將允許自旋等待持續相對更長的時間,比如100個cycle; case2)如果對于某個鎖,自旋很少成功獲得過, 那在以后要獲取這個鎖時將可能省略掉自旋過程,以避免浪費處理器資源; 【3.2】鎖消除 1)定義:鎖消除是指虛擬機即時編譯器在運行時,對一些代碼上要求同步,但是被檢查到不可能存在共享數據競爭的鎖進行消除;(干貨——引入鎖消除的概念) 2)鎖消除的主要判定依據:來源于逃逸分析的數據支持;如果判定在一段代碼中,堆上的所有數據都不會逃逸出去從而被其他線程訪問到,那就可以把它們當做棧上數據對待,認為它們是線程私有的,同步加鎖自然就無須進行了; 3)problem+solution 3.1)problem:程序員自己應該很清楚,怎么會在明知道不存在數據爭用的case下還要求同步呢? 3.2)solution:許多同步措施并不是程序員自己加入的,同步的代碼在java程序中的普遍程度早就超過了大部分人的想象;(干貨——許多同步措施并不是程序員自己加入的) 3.3)看個荔枝:這段code 僅僅是輸出3個字符串相加的結果,無論是源碼字面上還是程序語義上都沒有同步;(干貨——鎖消除的荔枝)
public class LockEliminateTest {// raw codepublic String concatString(String s1, String s2, String s3) {return s1 + s2 + s3;}// javac 轉化后的字符串連接操作public String concatString(String s1, String s2, String s3) {StringBuffer sb = new StringBuffer();sb.append(s1);sb.append(s2);sb.append(s3);return sb.toString();} }
?對以上代碼的分析(Analysis):
A1)對于 javac 轉化后的字符串連接操作代碼:?使用了同步,因為StringBuffer.append() 方法中都有一個同步塊,鎖就是sb對象。虛擬機觀察變量sb,很快就會發現他的動態作用域被限制在 concatString() 方法內部;也就是所 sb 的所有引用都不會逃逸到方法之外;
A2)所以,雖然這里有鎖,但是可以被安全地消除掉,在即時編譯之后,這段代碼就會忽略掉所有的同步而直接執行了;
【3.3】鎖粗化 1)problem:如果一系列的連續操作都對同一個對象反復加鎖和解鎖,甚至加鎖操作是出現在循環體中的,那即使沒有線程競爭,頻繁地進行互斥同步操作也會導致不必要的性能損耗; 2)鎖粗化的定義:如果虛擬機探測到有這樣一串零碎的操作都對同一個對象加鎖,將會把加鎖同步的范圍擴展(粗化)到整個操作序列的外部; 3)看個荔枝:以下面的代碼為例,就是擴展到第一個 append() 操作前直到最后一個 append()操作之后,這樣只需要加鎖一次就可以了;? // javac 轉化后的字符串連接操作public String concatString(String s1, String s2, String s3) {StringBuffer sb = new StringBuffer();sb.append(s1);sb.append(s2);sb.append(s3);return sb.toString();} 【3.4】輕量級鎖 1)重量級鎖定義:使用操作系統互斥量來實現的傳統鎖; 2)輕量級鎖的目的:是在沒有多線程競爭的前提下,減少傳統的重量級鎖使用操作系統互斥量產生的性能消耗;(干貨——輕量級鎖的作用) 3)HotSpot虛擬機的對象頭分為兩部分信息:(干貨——HotSpot虛擬機的對象頭分為兩部分信息) 3.1)第一部分:用于存儲對象自身的運行時數據,如哈希碼,GC分代年齡等;這部分數據的長度在32位和64位的虛擬機中分別為 32bit 和 64bit,官方稱它為 Mark Word,它是實現輕量級鎖和偏向鎖的關鍵;(干貨——Mark Word 是實現輕量級鎖和偏向鎖的關鍵) 3.2)第二部分:用于存儲指向方法區對象類型數據的指針,如果是數組對象的話,還會有一個額外的部分用于存儲數組長度; 3.3)對象頭信息是與對象自身定義的數據無關的額外存儲成本,考慮到虛擬機的空間效率,Mark Word 被設計成一個非固定的數據結構以便在極小的空間內存儲盡量多的信息,它會工具對象的狀態復用自己的存儲空間; 3.4)HotSpot 虛擬機對象頭Mark Word 如下圖所示:
4)在代碼進入同步塊的時候: 4.1)輕量級鎖的加鎖過程:(干貨——輕量級鎖的加鎖過程) step1)如果此同步對象沒有被鎖定(鎖標志位為01狀態):虛擬機首先將在當前線程的棧幀中建立一個名為 鎖記錄的空間,用于存儲對象目前的Mark Word 的拷貝; step2)然后,虛擬機將使用CAS 操作嘗試將對象的 Mark Word 更新為指向 Lock Record的指針; step3)如果這個更新工作成功了,那么這個線程就擁有了該對象的鎖,并且對象Mark Word的鎖標志位將轉變為 00,即表示 此對象處于輕量級鎖定狀態; step4)如果這個更新失敗了,虛擬機首先會檢查對象的Mark Word 是否指向當前線程的棧幀,如果只說明當前線程已經擁有了這個對象的鎖,那就可以直接進入同步塊繼續執行,否則說明這個鎖對象以及被其他線程搶占了。如果有兩條以上的線程爭用同一個鎖,那輕量級鎖就不再有效,要膨脹為重量級鎖,鎖標志的狀態值變為 10,Mark Word中存儲的就是指向重量級(互斥量)的指針,后面等待鎖的線程也要進入阻塞狀態; 4.2)輕量級鎖的解鎖過程:(干貨——輕量級鎖的解鎖過程,其解鎖過程也是通過CAS 操作來進行的) step1)如果對象的Mark Word仍然指向著線程的鎖記錄,那就用CAS 操作把對象當前的Mark Word 和 線程中復制的 Dispatched Mard Word替換回來; step2)如果替換成功,整個同步過程就over了; step3)如果替換失敗,說明有其他線程嘗試過獲取該鎖,那就要在釋放鎖的同時,喚醒被掛起的線程; Conclusion) C1)輕量級鎖能提升程序同步性能的依據是:?對于絕大部分的鎖,在整個同步周期內都是不存在競爭的; C2)如果沒有競爭,輕量級鎖使用CAS 操作避免了使用互斥量的開銷;但如果存在鎖競爭,除了互斥量的開銷外,還額外發生了CAS 操作,因此在有競爭的case下, 輕量級鎖會比傳統的重量級鎖更慢; 【3.5】偏向鎖 1)偏向鎖的目的:消除數據在無競爭情況下的同步原語,進一步提高程序的運行性能; 2)如果說輕量級鎖是在無競爭的情況使用CAS 操作去消除同步使用的互斥量:那偏向鎖就是在無競爭的情況下把整個同步都消除掉,連CAS 操作都不做了;(干貨——偏向鎖的定義) 3)偏向鎖的偏:?它的意思是這個鎖會偏向于 第一個獲得它的線程,如果在接下來的執行過程中,該鎖沒有被其他的線程獲取,則持有偏向鎖的線程將永遠不需要再進行同步; 4)偏向鎖的原理:若當前虛擬機啟用了偏向鎖,那么,當鎖對象第一次被線程獲取的時候,虛擬機將會把對象頭中的標志位設為01, 即偏向模式;同時使用CAS 操作把獲取到這個鎖的線程的ID 記錄在對象的 Mark Word之中,如果 CAS操作成功,持有偏向鎖的線程以后每次進入這個鎖相關的同步塊時,虛擬機都可以不再進行任何同步操作;(干貨——偏向鎖的原理) 5)當有另一個線程去嘗試獲取這個鎖時,偏向模式就結束了:根據鎖對象目前是否處于被鎖定的狀態, 撤銷偏向后恢復到未鎖定(標志位為01)或輕量級鎖定(標志位為00)的狀態,后續的同步操作就如上面介紹的輕量級鎖那樣執行; Conclusion) C1)偏向鎖可以提高帶有同步但無競爭的程序性能; C2)如果程序中大多數的鎖總是被多個不同的線程訪問:那偏向模式是多余的;
總結
以上是生活随笔為你收集整理的jvm(13)-线程安全与锁优化的全部內容,希望文章能夠幫你解決所遇到的問題。
 
                            
                        - 上一篇: 网站没有备案怎么申请广告(网站没有备案怎
- 下一篇: 官网验证怎么操作(怎么官方验证)
