synchronized剖析

synchronized

在《Thinking in Java》 中,是這麼說的:對於併發工作,你需要某種方式來防止兩個任務訪問相同的資源(其實就是共享資源競爭)。防止這種衝突的方法就是當資源被一個任務使用時,在其上加鎖。第一個訪問某項資源的任務必須鎖定這項資源,使其他任務在其被解鎖之前,就無法訪問它了,而在其被解鎖之時,另一個任務就可以鎖定並使用它了。

synchronized的基本使用

  • 修飾方法
  • 修飾代碼塊

Synchronized的作用主要有三個:(1)確保線程互斥的訪問同步代碼(2)保證共享變量的修改能夠及時可見(3)有效解決重排序問題。

請分析一下同步方法和同步代碼塊的區別是什麼?

  • 語法不同
  • 同步方法的鎖:類名.class(靜態方法)、this(非靜態方法),而同步代碼塊的鎖可以指定
  • 鎖的粒度不同,在考慮性能方面,最好使用同步塊來減少鎖定範圍提高併發效率。我們可以選擇只同步會發生同步問題的部分代碼而不是整個方法。

synchronized底層原理

java虛擬機中同步Synchronization基於進入和退出管理(Monitor,監視器鎖)對象實現的(首先要先執行monitorenter指令,退出的時候monitorexit指令)
在這裏插入圖片描述
通過分析之後可以看出,使用Synchronized進行同步,其關鍵就是必須要對對象的監視器monitor進行獲取,當線程獲取monitor後才能繼續往下執行,否則就只能等待。而這個獲取的過程是互斥的,即同一時刻只有一個線程能夠獲取到monitor。我們可以看到上述字節碼中包含一個monitorenter指令以及多個monitorexit指令。這是因爲Java虛擬機需要確保所獲得的鎖在正常執行路徑,以及異常執行路徑上都能夠被解鎖。

synchronized優化(jdk1.6)

在JDK1.5中,synchronized是性能低效的。因爲這是一個重量級操作,它對性能最大的影響是阻塞的是實現,掛起線程和恢復線程的操作都需要轉入內核態中完成,這些操作給系統的併發性帶來了很大的壓力。相比之下使用Java提供的Lock對象,性能更高一些。

到了JDK1.6,發生了變化,對synchronized加入了很多優化措施,有自適應自旋,鎖消除,鎖粗化,輕量級鎖,偏向鎖等等。導致在JDK1.6上synchronize的性能並不比Lock差。

對象頭

java的對象頭由以下三部分組成:

1、Mark Word

2、指向類的指針

3、數組長度(只有數組對象纔有)

在同步的時候是獲取對象的monitor,即獲取到對象的鎖。那麼對象的鎖怎麼理解?無非就是類似對對象的一個標誌,那麼這個標誌就是存放在Java對象的對象頭。Java對象頭裏的Mark Word裏默認的存放的對象的Hashcode,分代年齡和鎖標記位。32位JVM Mark Word默認存儲結構爲:
在這裏插入圖片描述
如圖在Mark Word會默認存放hasdcode,年齡值以及鎖標誌位等信息。 Java SE 1.6中,鎖一共有4種狀態,級別從低到高依次是:無鎖狀態、偏向鎖狀態、輕量級鎖狀態和重量級鎖狀態,這幾個狀態會隨着競爭情況逐漸升級。鎖可以升級但不能降級,意味着偏向鎖升級成輕量級鎖後不能降級成偏向鎖。這種鎖升級卻不能降級的策略,目的是爲了提高獲得鎖和釋放鎖的效率。對象的MarkWord變化爲下圖
在這裏插入圖片描述

無鎖、偏向鎖、輕量級鎖、重量級鎖有什麼差別?升級過程如何?

  • 無鎖:是指沒有對資源進行鎖定,所有的線程都能訪問並修改同一個資源,但同時只有一個線程能修改成功。
  • 偏向鎖:偏向鎖是四種狀態中最樂觀的一種鎖:從始至終只有一個線程請求某一把鎖。(這就好比你在私家莊園裏裝了個紅綠燈,並且莊園裏只有你在開車。偏向鎖的做法便是在紅綠燈處識別來車的車牌號。如果匹配到你的車牌號,那麼直接亮綠燈。)
    當有第二個線程進入同步代碼塊時,則升級爲輕量級鎖
  • 輕量級鎖:多個線程在不同的時間段請求同一把鎖,也就是說沒有鎖競爭。針對這種情況,JVM採用了輕量級鎖,來避免線程的阻塞以及喚醒。(本來有一個線程,又來了一個線程自旋)
    如果鎖競爭情況嚴重,某個達到最大自旋次數的線程,會將輕量級鎖升級爲重量級鎖(依然是CAS修改鎖標誌位,但不修改持有鎖的線程ID)。當後續線程嘗試獲取鎖時,發現被佔用的鎖是重量級鎖,則直接將自己掛起(而不是忙等),等待將來被喚醒。
  • 重量級鎖是指當有一個線程獲取鎖之後,其餘所有等待獲取該鎖的線程都會處於阻塞狀態。
  1. 重量級鎖會阻塞、喚醒請求加鎖的線程。它針對的是多個線程同時競爭同一把鎖的情況。
    JVM採用了自適應自旋,來避免線程在面對非常小的synchronized代碼塊時,仍會被阻塞、
    喚醒的情況。
  2. 輕量級鎖採用CAS操作,將鎖對象的標記字段替換爲一個指針,指向當前線程棧上的一塊空
    間,存儲着鎖對象原本的標記字段。它針對的是多個線程在不同時間段申請同一把鎖的情
    況。
  3. 偏向鎖只會在第一次請求時採用CAS操作,在鎖對象的標記字段中記錄下當前線程的地址。
    在之後的運行過程中,持有該偏向鎖的線程的加鎖操作將直接返回。它針對的是鎖僅會被同
    一線程持有的情況。

鎖粗化

鎖粗化就是將多次連接在一起的加鎖、解鎖操作合併爲一次,將多個連續的鎖擴展成爲一個範圍更大的鎖。

public class Test{
	private static StringBuffer sb = new StringBuffer();
	public static void main(String[] args) {
		sb.append("a");
		sb.append("b");
		sb.append("c");
	}
}

這裏每次調用stringBuffer.append方法都需要加鎖和解鎖,如果虛擬機檢測到有一系列連串的對同一個對象加鎖和解鎖操作,就會將其合併成一次範圍更大的加鎖和解鎖操作,即在第一次append方法時進行加鎖,最後一次append方法結束後進行解鎖。

鎖消除

鎖消除即刪除不必要的加鎖操作。根據代碼逃逸技術,如果判斷到一段代碼中,堆上的數據不會逃逸出當前線程,那麼可以認爲這段代碼是線程安全的,不必要加鎖。看下面這段程序:

public class Test{
	public static void main(String[] args) {
		StringBuffer sb = new StringBuffer();
		sb.append("a").append("b").append("c");
	}
}

雖然StringBuffer的append是一個同步方法,但是這段程序中的StringBuffer屬於一個局部變量,並且不會從該方法中逃逸出去,所以其實這過程是線程安全的,可以將鎖消除。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章