前言:為什么使用synchronized?
在多線程編程中,一個資源會被多個線程共享,為了避免因為資源搶占導致數據錯亂,所以要對線程進行同步。因此,引入synchronized關鍵字。
以下,來探究下synchronized的使用和底層原理。
一、synchronized的作用
1.1原子性
原子性:指一個操作或多個操作,要么全部執行,要么全部不執行。
java中,賦值和讀取值的操作都是原子的。
但是i++,i+=1等操作不是原子的,因為這些操作在底層是不是一個操作,而是被分成了讀取,計算,賦值幾步操作,所以保證這幾步的原子性,才能保證i++的原子性。
注意:synchronized 可以保證原子性,但是volatile不能保證原子性。
1.2 可見性
可見性:指多線程訪問同一資源時,該資源對所有線程都是可見的。
synchronized對一個資源加鎖之后,在釋放鎖之前會將變量的修改刷新到主內存中,保證資源的可見的。
1.3 有序性
有序性:指程序中的代碼會按照一定的順序執行。
在java中,有編譯器重排,指令級重排以及系統重排,這些指令重排會使指令實際執行的順序與實際可見的順序不同。
synchronized保證了指令不會被重排,按照顯示的順序執行。
注:synchronized是可重入的。即一個線程已經獲取該資源的synchronized鎖時,還可以再次獲得該資源的鎖。
二、synchronized的作用范圍
synchronized關鍵字可以修飾靜態方法,實例函數,代碼塊。
public class SyncDemo {
// 修飾靜態方法,對類加鎖
public static synchronized void test1(){
}
// 修飾實例方法,對實例對象加鎖
public synchronized void test2(){
//修飾代碼塊,對類加鎖
synchronized (SyncDemo.class){
}
}
public void test3(){
// 修飾代碼塊,對當前對象,即實例方法加鎖
synchronized (this){
}
}
public static void main(String[] args) {
}
}
2.1 修飾靜態方法
由類加載機制可以知道,靜態方法是和類同時加載的,歸屬于類。所以當synchronized修飾靜態方法時,是對類加鎖。
2.2 修飾實例方法
synchronized修飾實例方法 ,即對當前實例方法加鎖。
2.3 修飾代碼塊
如上代碼:修飾的第一個代碼塊是對實例方法加鎖,修飾的第二個代碼塊是對類加鎖。所以修飾代碼塊時,是可以選擇加鎖對象的。
三、synchronized底層原理
我們將上述代碼反編譯成字節碼,看看底層實現原理。
從class字節碼文件可以看出,一個通過方法flags標志,一個是通過monitorenter和monitorexit指令。
3.1 修飾實例方法
可以看出,synchronized修飾實例方法時,是在方法的flags里面加了一個ACC_SYNCHRONIZED標志。
此標志表示JVM這是一個同步方法,該線程進入該方法時,需要先獲取對應的鎖,且鎖計數器加1,釋放時減1.
3.2 修飾代碼塊
從反編譯的字節碼可以看出,同步代碼塊是由monitorexit指令進入,然后monitorexit釋放鎖。
但是截圖中可以看出,有兩個monitorexit,這里為什么有兩個monitorexit?
第二個monitorexit是來處理異常的。正常情況下,第一個monitorexit之后會執行goto指令,而該指令的返回是后面的return。正常情況下只會執行第一個monitorexit釋放鎖,然后返回。
而如果執行中發生了異常,第二個monitorexit起作用了,它由編譯器自動生成,發生異常時處理異常,然后釋放鎖的。
四、synchronized鎖的底層原理實現
上面我們了解了JVM中如何實現synchronized鎖的,但是JVM中如何對對象加鎖的呢?
JVM中,對象由三部分構成:對象頭,實例數據,對齊填充。
先簡單介紹下實例數據和對齊填充:
實例數據:存放類的屬性數據信息,包括父類的屬性信息。如果是數組實例,還包括數組的長度。
對齊填充:不是必需部分,是虛擬機要求對象地址是8字節的整數倍,所以僅僅是用來字節對齊。
對象頭:包括兩個部分,Mark Word和Class Metadata Address。Mark Word存儲對象的hashCode,鎖信息,和分代年齡等信息;而后者記錄指針指向對象的類元信息,即確定對象是哪個類的實例。
鎖的狀態
額外說明下鎖的狀態,在JDK1.6之前,鎖只有無鎖和重量級鎖兩個狀態。在JDK1.6之后,對synchronized鎖進行了優化,鎖狀態有四個:無鎖狀態,偏向鎖,輕量級鎖,重量級鎖。
五、synchronized鎖的優化
因為JDK1.6之前,只有重量級鎖,所以使用synchronized會造成很大的消耗,所以之后,對synchronized鎖進行了優化。
5.1 偏向鎖
針對使用了synchronized鎖,但是實際操作時,不存在鎖競爭時,會加上偏向鎖。頭對象會標記偏向鎖的狀態位,見上圖。
這樣就減少了同一線程獲取鎖的代價。
5.2 輕量級鎖
由偏向鎖升級而來,當存在第二個鎖申請同一個鎖對象時,偏向鎖就會升級為輕量級鎖。
5.3 重量級鎖
由輕量級鎖升級而來,當同一時間有多個線程競爭鎖時,鎖會倍升級為重量級鎖。
5.4 鎖升級
鎖的狀態會一步步升級,無鎖——>偏向鎖——> 輕量級鎖——> 重量級鎖。而且鎖的升級是不可逆的。即升級到輕量級鎖,重量級鎖,是無法自動恢復到無鎖,偏向鎖的狀態的。
5.5 鎖消除
鎖消除是JVM另一種鎖優化機制,指編譯時,對上下文的分析,去除不可能存在競爭的鎖。
5.6 鎖粗化
鎖粗化也是JVM的一種鎖優化機制,通過擴大鎖的范圍,避免反復的加鎖和釋放鎖。