您好,登錄后才能下訂單哦!
這期內容當中小編將會給大家帶來有關怎么用Java實現synchronized鎖同步機制,文章內容豐富且以專業的角度為大家分析和敘述,閱讀完這篇文章希望大家可以有所收獲。
synchronized 是通過進入和退出 Monitor 對象實現鎖機制,代碼塊通過一對 monitorenter/monitorexit 指令實現。在編譯后,monitorenter 指令插入到同步代碼塊的開始位置,monitorexit 指令插入到方法結束和異常處,JVM 要保證 monitorenter 和 monitorexit 成對出現。任何對象都有一個 Monitor 與之關聯,當且僅當一個 Monitor 被持有后,它將處于鎖狀態。
在執行 monitorenter 時,首先嘗試獲取對象的鎖,如果對象沒有被鎖定或者當前線程持有鎖,鎖的計數器加 1;相應的,在執行 monitorexit 指令時,將鎖的計數器減 1。當計數器減到 0 時,鎖釋放。如果在 monitorenter 獲取鎖失敗,當前線程會被阻塞,直到對象鎖被釋放。
在 JDK6 之前,Monitor 的實現是依靠操作系統內部的互斥鎖實現(一般使用的是 Mutex Lock 實現),線程阻塞會進行用戶態和內核態的切換,所以同步操作是一個無差別的重量級鎖。
后來,JDK 對 synchronized 進行升級,為了避免線程阻塞時在用戶態與內核態之間切換線程,會在操作系統阻塞線程前,加入自旋操作。然后還實現 3 種不同的 Monitor:偏向鎖(Biased Locking)、輕量級鎖(Lightweight Locking)、重量級鎖。在 JDK6 之后,synchronized 的性能得到很大的提升,相比于 ReentrantLock 而言,性能并不差,只不過 ReentrantLock 使用起來更加靈活。
synchronized 對性能影響最大的是阻塞的實現,掛起線程和恢復線程都需要操作系統幫助完成,需要從用戶態轉到內核態,狀態轉換需要耗費很多 CPU 時間。
在我們大多數的應用中,共享數據的鎖定狀態只會持續很短的一段時間,為了這段時間掛起和回復線程消耗的時間不值得。而且,現在大多數的處理器都是多核處理器,如果讓后一個線程再等一會,不釋放 CPU,等前一個釋放鎖,后一個線程立馬獲取鎖執行任務就行。這就是所謂的自旋,讓線程執行一個忙循環,自己在原地轉一會,每轉一圈看看鎖釋放沒有,釋放了直接獲取鎖,沒有釋放就再轉一圈。
自旋鎖是在 JDK 1.4.2 引入(使用-XX:+UseSpinning參數打開),JDK 1.6 默認打開。自旋鎖不能代替阻塞,因為自旋等待雖然避免了線程切換的開銷,但是它要占用 CPU 時間,如果鎖占用時間短,自旋等待效果挺好,反之,則是性能浪費。所以在 JDK 1.6 中引入了自適應自旋鎖:如果同一個鎖對象,自旋等待剛成功,且持有鎖的線程正在運行,那本次自旋很有可能成功,會允許自旋等待持續時間長一些。反之,如果對于某個鎖,自旋很少成功,那之后很有可能直接省略自旋過程,避免浪費 CPU 資源。
synchronized 用的鎖存在于 Java 對象頭里,對象頭里的 Mark Word 里存儲的數據會隨標志位的變化而變化,變化如下:
Java 對象頭 Mark Word
大多數情況下,鎖不僅不存在多線程競爭,而且總是由同一線程多次獲得,為了讓線程獲得鎖的代價更低,引入偏向鎖。
當一個線程訪問同步塊并獲取鎖時,會在對象頭和棧幀中的鎖記錄里存儲鎖偏向的線程 ID,以后該線程在進入和退出同步塊時不需要進行 CAS 操作來加鎖和解鎖,只需簡單地測試一下對象頭的 Mark Word 里是否存儲著指向當前線程的偏向鎖。引入偏向鎖是為了在無多線程競爭的情況下盡量減少不必要的輕量級鎖執行路徑,因為輕量級鎖的獲取及釋放依賴多次 CAS 原子指令,而偏向鎖只需要在置換 ThreadID 的時候依賴一次 CAS 原子指令(由于一旦出現多線程競爭的情況就必須撤銷偏向鎖,所以偏向鎖的撤銷操作的性能損耗必須小于節省下來的 CAS 原子指令的性能消耗)。
當鎖對象第一次被線程獲取時,對象頭的標志位設為 01,偏向模式設為 1,表示進入偏向模式。
測試線程 ID 是否指向當前線程,如果是,執行同步代碼塊,如果否,進入 3
使用 CAS 操作把獲得到的這個鎖的線程 ID 記錄在對象的 Mark Word 中。如果成功,執行同步代碼塊,如果失敗,說明存在過其他線程持有鎖對象的偏向鎖,開始嘗試當前線程獲取偏向鎖
當到達全局安全點時(沒有字節碼正在執行),會暫停擁有偏向鎖的線程,檢查線程狀態。如果線程已經結束,則將對象頭設置成無鎖狀態(標志位為“01”),然后重新偏向新的線程;如果線程仍然活著,撤銷偏向鎖后升級到輕量級鎖狀態(標志位為“00”),此時輕量級鎖由原持有偏向鎖的線程持有,繼續執行其同步代碼,而正在競爭的線程會進入自旋等待獲得該輕量級鎖。
偏向鎖的釋放采用的是惰性釋放機制:只有等到競爭出現,才釋放偏向鎖。釋放過程就是上面說的第 4 步,這里不再贅述。
偏斜鎖并不適合所有應用場景,撤銷操作(revoke)是比較重的行為,只有當存在較多不會真正競爭的同步塊時,才能體現出明顯改善。實踐中對于偏斜鎖的一直是有爭議的,有人甚至認為,當你需要大量使用并發類庫時,往往意味著你不需要偏斜鎖。
所以如果你確定應用程序里的鎖通常情況下處于競爭狀態,可以通過 JVM 參數關閉偏向鎖:-XX:-UseBiasedLocking=false,那么程序默認會進入輕量級鎖狀態。
輕量級鎖不是用來代替重量級鎖的,它的初衷是在沒有多線程競爭的前提下,減少傳統的重量級鎖使用操作系統互斥量產生的性能損耗。
如果同步對象鎖狀態為無鎖狀態(鎖標志位為“01”狀態,是否為偏向鎖為“0”),虛擬機首先將在當前線程的棧幀中建立一個名為鎖記錄(Lock Record)的空間,用于存儲鎖對象目前的 Mark Word 的拷貝,官方稱之為 Displaced Mark Word。這時候線程堆棧與對象頭的狀態如下圖所示:
拷貝對象頭中的 Mark Word 復制到鎖記錄(Lock Record)中。
拷貝成功后,虛擬機將使用 CAS 操作嘗試將對象的 Mark Word 更新為指向 Lock Record 的指針,并將 Lock record 里的 owner 指針指向 object mark word。
如果成功,當前線程持有該對象鎖,將對象頭的 Mark Word 鎖標志位設置為“00”,表示對象處于輕量級鎖定狀態,執行同步代碼塊。這時候線程堆棧與對象頭的狀態如下圖所示:
如果更新失敗,檢查對象頭的 Mark Word 是否指向當前線程的棧幀,如果是,說明當前線程擁有鎖,直接執行同步代碼塊。
如果否,說明多個線程競爭鎖,如果當前只有一個等待線程,通過自旋嘗試獲取鎖。當自旋超過一定次數,或又來一個線程競爭鎖,輕量級鎖膨脹為重量級鎖。重量級鎖使除了擁有鎖的線程以外的線程都阻塞,防止 CPU 空轉,鎖標志的狀態值變為“10”,Mark Word 中存儲的就是指向重量級鎖(互斥量)的指針,后面等待鎖的線程也要進入阻塞狀態。
輕量級鎖解鎖的時機是,當前線程同步塊執行完畢。
通過 CAS 操作嘗試把線程中復制的 Displaced Mark Word 對象替換當前的 Mark Word。
如果成功,整個同步過程完成
如果失敗,說明存在競爭,且鎖膨脹為重量級鎖。釋放鎖的同時,會喚醒被掛起的線程。
輕量級鎖適應的場景是線程近乎交替執行同步塊的情況,如果存在同一時間訪問相同鎖對象時(第一個線程持有鎖,第二個線程自旋超過一定次數),輕量級鎖會膨脹為重量級鎖,Mark Word 的鎖標記位更新為 10,Mark Word 指向互斥量(重量級鎖)。
重量級鎖是通過對象內部的一個叫做監視器鎖(monitor)來實現的,監視器鎖本質又是依賴于底層的操作系統的 Mutex Lock(互斥鎖)。操作系統實現線程之間的切換需要從用戶態轉換到核心態,這個成本非常高,狀態之間的轉換需要相對比較長的時間,這就是為什么 JDK 1.6 之前,synchronized 重量級鎖效率低的原因。
下圖是偏向鎖、輕量級鎖、重量級鎖之間轉換對象頭 Mark Word 數據轉變:
偏向鎖、輕量級鎖、重量級鎖之間轉換
網上有一個比較全的鎖升級過程:
鎖升級過程
鎖消除說的是虛擬機即時編譯器在運行過程中,對于一些同步代碼,如果檢測到不可能存在共享數據競爭情況,就會刪除鎖。也就是說,即時編譯器根據情況刪除不必要的加鎖操作。
鎖消除的依據是逃逸分析。簡單地說,逃逸分析就是分析對象的動態作用域。分三種情況:
不逃逸:對象的作用域只在本線程本方法
方法逃逸:對象在方法內定義后,被外部方法所引用
線程逃逸:對象在方法內定義后,被外部線程所引用
即時編譯器會針對對象的不同情況進行優化處理:
對象棧上分配(Stack Allocations,HotSpot 不支持):直接在棧上創建對象。
標量替換(Scalar Replacement):將對象拆散,直接創建被方法使用的成員變量。前提是對象不會逃逸出方法范圍。
同步消除(Synchronization Elimination):就是鎖消除,前提是對象不會逃逸出線程。
對于鎖消除來說,就是逃逸分析中,那些不會逃出線程的加鎖對象,就可以直接刪除同步鎖。
通過代碼看一個例子:
public void elimination1() { final Object lock = new Object(); synchronized (lock) { System.out.println("lock 對象沒有只會作用域本線程,所以會鎖消除。"); } } public String elimination2() { final StringBuffer sb = new StringBuffer(); sb.append("Hello, ").append("World!"); return sb.toString(); } public StringBuffer notElimination() { final StringBuffer sb = new StringBuffer(); sb.append("Hello, ").append("World!"); return sb; }
elimination1()中的鎖對象lock作用域只是方法內,沒有逃逸出線程,elimination2()中的sb也就這樣,所以這兩個方法的同步鎖都會被消除。但是notElimination()方法中的sb是方法返回值,可能會被其他方法修改或者其他線程修改,所以,單看這個方法,不會消除鎖,還得看調用方法。
原則上,我們在編寫代碼的時候,要將同步塊作用域的作用范圍限制的盡量小。使得需要同步的操作數量盡量少,當存在鎖競爭時,等待線程盡快獲取鎖。但是有時候,如果一系列的連續操作都對同一個對象反復加鎖和解鎖,甚至加鎖操作是出現在循環體中的,那即使沒有出現線程競爭,頻繁地進行互斥同步操作也會導致不必要的性能損耗。如果虛擬機檢測到有一串零碎的操作都是對同一對象的加鎖,將會把加鎖同步的范圍擴展(粗化)到整個操作序列的外部。
比如上面例子中的elimination2()方法中,StringBuffer的append是同步方法,頻繁操作時,會進行鎖粗化,最后結果會類似于(只是類似,不是真實情況):
public String elimination2() { final StringBuilder sb = new StringBuilder(); synchronized (sb) { sb.append("Hello, ").append("World!"); return sb.toString(); } }
或者
public synchronized String elimination3() { final StringBuilder sb = new StringBuilder(); sb.append("Hello, ").append("World!"); return sb.toString(); }
同步操作中影響性能的有兩點:
加鎖解鎖過程需要額外操作
用戶態與內核態之間轉換代價比較大
synchronized 在 JDK 1.6 中有大量優化:分級鎖(偏向鎖、輕量級鎖、重量級鎖)、鎖消除、鎖粗化等。
synchronized 復用了對象頭的 Mark Word 狀態位,實現不同等級的鎖實現。
上述就是小編為大家分享的怎么用Java實現synchronized鎖同步機制了,如果剛好有類似的疑惑,不妨參照上述分析進行理解。如果想知道更多相關知識,歡迎關注億速云行業資訊頻道。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。