Java并發編程:Synchronized 的實現原理
在剛開始學習 Java 并發編程的過程中,一遇到多線程,我們就會使用 synchronized 關鍵字。在 JDK1.5 之前,Synchronized 是一個重量級鎖,效率不盡如人意。JDK1.6 對 Synchronized 鎖進行了升級優化,引入了偏向鎖和輕量級鎖,提高了獲取鎖和釋放鎖的效率。下面我們來看一看 Synchronized 的底層實現原理吧。
Synchronized 的底層實現原理
同步原理
我們先來反編譯下面的 method1 方法:
public void method1() {
synchronized (this) {
System.out.println("This is the synchronized");
}
}
在下面,我們可以看到反編譯后的 method1 代碼:
圖片
從上面代碼執行過程中,我們看到代碼塊同步是使用 monitorenter 和 monitorexit 指令實現的。
monitorenter 指令是在編譯后插入到同步代碼塊的開始位置,monitorexit 是插入到方法結束的位置或者異常處。
JVM 要保證每個 monitorenter 必須有對應的 monitorexit 與之配對。任何對象都有一個 monitor 對象與之關聯,當 monitor 被對象持有后,它將處于鎖定狀態。線程執行到 monitorenter 指令時,將會嘗試獲取對象所對應的 monitor 的所有權,即嘗試獲得對象的鎖。
我們再看一下 Synchronized 方法同步的步驟:
public synchronized void method2() {
System.out.println("This is the synchronized method");
}
圖片
在上面編譯過的 method2() 方法的執行過程中,Synchronized 方法的同步是使用另外一種方式實現的,我們可以看到它被翻譯成普通的方法調用和返回 invokevirtual、return 指令。
一個被 Synchronized 修飾的方法在 JVM 底層并沒有與之對應的字節碼指令。當方法調用時,調用指令將會檢查方法的 ACC_SYNCHRONIZED 訪問標志是否被設置,如果設置了執行線程將先獲取 monitor,獲取成功之后,才能執行方法,方法執行完成再釋放 monitor。
Java 對象頭的概念
接下來,我們再來了解一下 Java 對象頭的概念。Synchronized 用的鎖就是存放在 Java 對象頭里。如果對象是數組類型,則虛擬機用 3 個字寬(Word)存儲對象頭,如果對象是非數組類型,則用 2 字寬存儲對象頭。在 32 位虛擬機中,1 字寬等于 4 字節,即 32bit。
我們看下面的表格,Java 對象頭主要包括兩部分數據:Mark Word(標記字段)、Class Pointer(類型指針)。Mark Word 用于存儲對象自身的運行時數據,它是實現輕量級鎖和偏向鎖的關鍵。Class Pointer 是對象指向它的類元數據的指針,虛擬機通過這個指針來確定這個對象是哪個類的實例。
圖片
Java 對象頭里的 Mark Word 里默認存儲對象的 HashCode、分代年齡和鎖標記位。我們來看一下 32 位 JVM 的 Mark Word 的默認存儲結構:
圖片
在運行期間,Mark Word 里存儲的數據會隨著鎖標志位的變化而變化。Mark Word 可能變化為存儲以下 4 種數據,我們來看一下存儲結構:
圖片
在 64 位虛擬機下,Mark Word 是 64bit 大小的,我們看一下它的存儲結構:
圖片
我們從上面的表格中看到了引入的偏向鎖和輕量級鎖。鎖級別從低到高依次為:無鎖狀態、偏向鎖狀態、輕量級鎖狀態和重量級鎖狀態,這 4 種狀態是會隨著競爭情況而逐漸升級的。鎖可以升級但不能降級,目的是提高獲得鎖和釋放鎖的效率。
下面我們來了解一下鎖升級的流程。
鎖升級
Synchronized 內部有一個隱藏的鎖升級流程,正是因為這個流程的存在,使得 Synchronized 得以發揮它的高性能特性。鎖升級中最重要的 2 個升級就是偏向鎖和輕量級鎖,下面我們分別展開討論:
偏向鎖
通常情況下,鎖不僅不存在多線程競爭,而且總是由同一線程多次獲得,為了讓線程更容易獲得鎖而引入了偏向鎖。
所謂偏向鎖,就是當一個線程訪問同步代碼塊并獲取鎖時,會在對象頭存儲鎖偏向的線程 ID。這樣,以后該線程在進入和退出同步塊時,就不需要進行 CAS 操作來加鎖和解鎖,只需要簡單地測試一下對象頭的 Mark Word 里是否存儲著指向當前線程的偏向鎖。
如果測試成功,則表示線程已經獲得了鎖;如果測試失敗,則需要再測試一下 Mark Word 中偏向鎖的標識是否設置成 1(表示當前是偏向鎖)。如果沒有設置,則使用 CAS 競爭鎖;如果設置了,則嘗試使用 CAS 將對象頭的偏向鎖指向當前線程。
偏向鎖在 Java6 和 Java7 是默認啟用的,但它在應用程序啟動幾秒鐘之后才會被激活,我們可以配置 JVM 參數來關閉延遲:-XX:BiasedLockingStartupDelay=0。如果確定應用程序里所有的鎖通常情況下處于競爭狀態,我們可以配置如下的 JVM 參數關閉偏向鎖,之后程序默認會進入輕量級鎖狀態:
-XX:-UseBiasedLocking=false
輕量級鎖
輕量級鎖不是用來替代傳統的重量級鎖的,而是在沒有多線程競爭的情況下,使用輕量級鎖能夠減少性能消耗,但是當多個線程同時競爭鎖時,輕量級鎖會膨脹為重量級鎖。
我們先來看一下輕量級鎖的加鎖過程。在線程在執行同步塊之前,JVM 會先在當前線程的棧幀中創建用于存儲鎖記錄的空間,并將對象頭中的 Mark Word 復制到鎖記錄中,官方稱為 Displaced Mark Word。然后線程嘗試使用 CAS 將對象頭中的 Mark Word 替換為指向鎖記錄的指針。如果成功,當前線程獲
得鎖,如果失敗,表示其他線程競爭鎖,當前線程便嘗試使用自旋來獲取鎖。
輕量級解鎖時,會使用原子的 CAS 操作將 Displaced Mark Word 替換回到對象頭,如果成功,則表示沒有競爭發生。如果失敗,表示當前鎖存在競爭,鎖就會膨脹成重量級鎖。
因為自旋會消耗 CPU,為了避免無用的自旋,一旦鎖升級到重量級鎖,就不會再恢復到輕量級鎖狀態。當鎖處于這個狀態下,其他線程嘗試獲取鎖時,都會被阻塞住,當持有鎖的線程釋放鎖之后會喚醒這些線程,被喚醒的線程就會進行新一輪的競爭。
下圖是兩個線程同時爭奪鎖,導致輕量級鎖膨脹的流程。
圖片
鎖的優缺點對比
對于偏向鎖、輕量級鎖和重量級鎖這三者的優缺點,以及適用場景,我們可以通過下面的表格得到一個直觀的了解:
圖片
總結
最后,我們來總結一下所講的主要內容。首先,我們一起學習了 Synchronized 的底層實現,Synchronized 作為一個關鍵字以它極簡的語法也帶來了易讀性;之后,我帶你了解了偏向鎖的初始化、撤銷、關閉操作和輕量級鎖的加鎖、解鎖過程;最后,我帶你分析了不同鎖的優缺點及適用場景,這些對你理解為什么 Synchronized 具備高性能是非常關鍵的。此外,不同鎖的,如偏向鎖、輕量級鎖對使用者來說是透明的,這也體現了 Synchronized 的簡單性。
隨著 JDK 的不斷發展,Synchronized 已經做了足夠多的性能優化。Synchronized 從一個開銷很大的重量級鎖被優化成一個可自動適配場景的“智能”鎖,它可以根據場景轉換成偏向鎖、輕量級鎖,萬不得已的情況下才會轉換成重量級鎖。它的應用場景也隨著這些特性逐漸豐富起來,在很多高并發場景甚至替代了 reentrantLock。