synchronized
關鍵字提供了一種獨佔式的加鎖方式,用來控制多個線程對共享資源的互斥訪問。它能夠保證在同一時刻只有一個線程在執行該段代碼,同時它還能夠保證共享變量的內存可見性。java
synchronized
的獲取和釋放鎖由 JVM
實現,用戶不須要顯示的獲取和釋放鎖,很是方便。可是當線程嘗試獲取鎖的時候,若是獲取不到鎖該線程會一直阻塞。數組
在早期版本中,synchronized
是一個重量級鎖,效率低下。但從 JDK1.6
開始,從 JVM
層面對 synchronized
引入了各類鎖優化技術,例如:自旋鎖、適應性自旋鎖、鎖消除、鎖粗化、輕量級鎖和偏向鎖等,大大減小了鎖操做的開銷。bash
使用 synchronized
實現同步有同步方法塊、同步方兩種方式。數據結構
做用於代碼塊時,括號中能夠是指定的對象,也能夠是 Class 對象。多線程
// 鎖的是指定的對象實例
public void test1 (){
synchronized(this) {
// ···
}
}
複製代碼
// 鎖定是指定的類對象
public void test2 (){
synchronized(Test.class) {
// ···
}
}
複製代碼
做用於方法時,鎖的是當前的對象實例。併發
public synchronized void test3(){
// ···
}
複製代碼
做用於靜態方法,鎖的是類對象。性能
public synchronized static void test4(){
// ···
}
複製代碼
HotSpot 對象頭優化
HotSpot
虛擬機的對象頭分爲兩部分信息:this
Mark Word
:用於存放對象自身的運行時數據,如哈希碼、GC
分代年齡、鎖類型、鎖標誌位等信息,這部分數據在 32
位和 64
位虛擬機中分別爲 32
和 64 bit
。它是實現輕量級鎖和偏向鎖的關鍵。Class Metadata Address
:用於存儲指向方法區對象類型數據的指針,若是是數組,還會有一個額外的部分用於存放數組長度。Mark Word
被設計爲一個非固定的數據結構以便存儲更多的信息,它會根據對象的狀態複用本身的存儲空間。例如,在 32
位的 HotSpot
虛擬機中,各類狀態下對象的存儲內容以下:spa
Monitor
每一個 Java
對象都有一個 Monitor
對象與之關聯,它被稱爲管程(監視器鎖),前面的表格中,鎖狀態爲重量級鎖時,指針就指向 Monitor
對象的起始地址。當一個 Monitor
被某個線程持有後,便處於鎖定狀態。在 HotSpot
虛擬機的源碼實現中,ObjectMonitor
對象相關屬性有:
_count
:計數器;_owner
:指向持有 ObjectMonitor
對象的線程;_WaitSet
:等待池;_EntryList
:鎖池;多個線程訪問同步代碼時,首先會進入 _EntryList
鎖池中被阻塞,當線程獲取到對象的 Monitor
後,就會把 _owner
指向當前線程,同時 Monitor
中的 _count
計數器加一。若是線程調用 wait
方法,_owner
就被恢復爲 null
,_count
計數器減一,同時該線程就會進入 _WaitSet
等待池中。
當線程執行完畢,將對應的變量復位,以便其餘線程獲取 Monitor 鎖。
四種狀態
synchronized
有四種狀態:無鎖、偏向鎖、輕量級鎖和重量級鎖。隨着對鎖的競爭逐漸激烈,鎖的狀態進行升級。
public class SynchronizedTest {
public void test1() {
synchronized (this) {
// ···
}
}
}
複製代碼
使用 javap -c -v
對 SynchronizedTest.class
進行反彙編:
能夠看到,在同步代碼塊的開始位置插入 monitorenter
指令,在結束位置插入 monitorexit
指令,並且必須保證每個 monitorenter
都有一個 monitorexit
與之對應。
synchronized
即是經過 Monitor
獲取鎖的。當線程執行到 monitorenter
指令時,將會嘗試獲取 Monitor
全部權。當計數器爲 0
,則成功獲取;獲取後將鎖計數器置爲 1
。在執行 monitorexit
指令時,將鎖計數器置爲 0
。若是獲取對象鎖失敗,那當前線程就要阻塞等待,直到鎖被另一個線程釋放爲止。
public class SynchronizedTest {
public synchronized void test1() {
// ···
}
}
複製代碼
使用 javap -c -v
對 SynchronizedTest.class
進行反彙編:
能夠看到,被同步的方法也僅是被翻譯成普通的方法調用和返回指令。在 JVM
字節碼層面並無任何特別的指令來實現 synchronized
修飾的方法。
可是在 Class
文件的方法表中將方法的 flags
字段中的 ACC_SYNCHRONIZED
標誌位置爲 1
,表示該方法是同步方法。在執行方法時,線程就會持有 Monitor
對象。
JDK1.6
對鎖引入了大量的優化,如自旋鎖、自適應自旋鎖、鎖消除、鎖粗化、輕量級鎖、偏向鎖等技術來減小鎖操做的開銷。
在實現同步互斥時,若是獲取鎖失敗,就會使當前線程阻塞,但線程的掛起和恢復都須要在內核態和用戶態之間轉換,對系統的性能影響很大。許多狀況下共享數據的鎖定狀態持續時間不會很長,切換線程不值得。
自旋鎖就是讓線程在請求共享數據的鎖時執行一個忙循環(自旋),若是可以很快得到鎖,就避免其進入阻塞狀態。
自旋等待雖然避免了線程切換的開銷,但它要求多處理器,並且要佔用處理器時間。若是鎖佔用時間過長,那麼反而會消耗更多的資源。所以,對自旋等待的時間必須進行限制,另外自旋的次數也不能過多,默認爲 10
次,可以使用 -XX:PreBlockSpin
參數修改。
JDK1.6
中引入了自適應的自旋鎖,它的自旋時間由前一次在同一個鎖上的自旋時間及鎖的擁有者的狀態來決定。
鎖消除是指虛擬機的即時編譯器在運行時,若是代碼要求同步,但檢測發現不可能存在共享數據競爭時,那麼就進行鎖消除。
鎖消除主要根據逃逸分析,若是判斷在一段代碼中,堆上的全部數據都不會逃逸出去,那就能夠將它們認爲是線程私有的,也就無須進行同步加鎖。
若是一系列的連續操做都對同一個對象反覆加鎖和解鎖,甚至加鎖也是出現循環體中,那麼即便沒有數據競爭,頻繁地加鎖解鎖也會致使沒必要須的性能消耗。
鎖粗化指的就是若是虛擬機探測到這樣的狀況,那就將加鎖同步的範圍擴展(粗化)到整個操做序列的外部。
偏向鎖是在無競爭的狀況下消除整個同步,也就是減小同一線程獲取鎖的代價。它的思想是這個鎖會偏向於第一個得到它的線程,若是接下來該鎖沒有被其餘的線程獲取,則持有偏向鎖的線程將永遠不須要再進行同步。
當鎖對象第一次被線程獲取時,鎖進入偏向模式,同時 Mard Word 的結構也變爲偏向鎖結構。鎖標誌位爲「01」,同時使用 CAS
操做把獲取到這個鎖的線程的 ID
記錄在對象的 Mark Word
中,若是 CAS
操做成功,這個線程之後每次進入這個鎖相關的同步塊時,均可以不用再進行任何同步操做。
不適用於鎖競爭比較激烈的多線程場合。
當有另一個線程去嘗試獲取這個鎖對象時,偏向狀態就宣告結束。根據鎖對象目前是否處於被鎖定的狀態,撤銷偏向後恢復到未鎖定狀態或者輕量級鎖狀態。
輕量級鎖是相對於使用操做系統互斥量實現的傳統鎖而言的。偏向鎖運行在一個線程進入同步塊時,若是有第二個線程加入鎖競爭,則偏向鎖就會升級爲輕量級鎖。它適用於線程交替執行的場景。
在代碼進入同步塊時,若是此同步對象沒有被鎖定(鎖標誌位爲「01」狀態),虛擬機將先在當前線程的棧幀中創建一個名爲鎖記錄(Lock Record
)的空間,用於存儲鎖對象目前 Mark Word
的拷貝。以下圖,左側是一個線程的虛擬機棧,右側是一個鎖對象:
而後,虛擬機將使用 CAS
操做嘗試將對象的 Mark Word
更新爲指向 Lock Record
的指針,並將 Lock Record
裏的 owner
指針指向對象的 Mark Word
。若是這個更新動做成功了,那麼這個線程就擁有了該對象的鎖,而且對象的 Mark Word
的鎖標誌位轉變爲「00」,即表示對象處於輕量級鎖定狀態。多線程堆棧和對象頭的狀態以下:
若是這個更新操做失敗了,虛擬機首先會檢查對象的 Mark Word 是否指向當前線程的棧幀,若是已指向則說明當前線程已經擁有了這個對象的鎖,那就能夠直接進入同步塊繼續執行,若是沒有指向則說明這個鎖對象已經被其餘對象搶佔了。
若是有兩條以上的線程爭用同一個鎖,那輕量級鎖就要膨脹爲重量級鎖,鎖標誌變爲「10」,Mark Word
中存儲的就是指向重量級鎖的指針,後面等待鎖的線程也要進入阻塞狀態。
對於絕大部分的鎖,在整個同步週期內都是不存在競爭的。若是沒有競爭,輕量級鎖使用 CAS
操做避免了重量級鎖使用互斥量的開銷,提高了程序同步的性能。
偏向鎖、輕量級鎖的狀態轉化及對象 Mark Word
的關係以下: