synchronized
給人的印象一直是并發(fā)編程中的元老級角色但是其比較重尽棕,并稱之為重量級鎖,但是事實真的是這樣嗎恩沽?其實隨著 Java SE 1.6 對 synchronized
進行了各種優(yōu)化葛碧,隨著 JDK 的升級對 synchronized
的優(yōu)化是持續(xù)進行的糯钙,因此對于 synchronized
的使用及理解就變得非常重要了,本文就對 synchronized
的原理進行一些介紹逞频。
先來看一下利用 synchronized
實現(xiàn)同步的基礎纯衍。Java 中的每一個對象都可以作為鎖。具體表現(xiàn)為 3 種形式苗胀。
- 對于普通同步方法襟诸,鎖是當前實例對象瓦堵。
- 對于靜態(tài)同步方法,鎖是當前類的 Class 對象歌亲。
- 對于同步方法塊菇用,鎖是
synchronized
括號里配置的對象。
當一個線程試圖訪問同步代碼塊時陷揪,它首先必須得到鎖惋鸥,退出或拋出異常時必須釋放鎖。那么鎖到底存在哪里呢悍缠?鎖里面會存儲什么信息呢卦绣?
從 JVM 規(guī)范中可以看到 synchronized
在 JVM 里的實現(xiàn)原理, JVM 基于進入和退出 Monitor 對象來實現(xiàn)方法同步和代碼塊同步扮休。方法同步和代碼塊同步是使用 monitorenter
和 monitorexit
指令實現(xiàn)的迎卤。 monitorenter
指令是在編譯后插入到同步代碼塊的開始位置拴鸵,而 monitorexit
是插入到代碼塊結(jié)束處和異常處玷坠, JVM 要保證每個 monitorenter
必須有對應的 monitorexit
與之配對。任何對象都有一個 monitor 與之關聯(lián)劲藐,并且一個 monitor 被持有后八堡,它將處于鎖定狀態(tài)。線程執(zhí)行到 monitorenter
指令時聘芜,將會嘗試獲取對象對應的 monitor 的所有權(quán)兄渺,即嘗試獲得對象的鎖。 例如對于如下簡單的代碼汰现,編譯出成字節(jié)碼之后再通過 javap -c 命令進行反匯編便可以看到 monitorenter
和 monitorexit
指令挂谍。
public class SynchronizedTest {
public static void main(String[] args) {
synchronized (SynchronizedTest.class) {
System.out.println("synchronized...");
}
}
}
Compiled from "SynchronizedTest.java"
public class com.weiqiang.SynchronizedTest {
public com.weiqiang.SynchronizedTest();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public static void main(java.lang.String[]);
Code:
0: ldc #2 // class com/weiqiang/SynchronizedTest
2: dup
3: astore_1
4: monitorenter
5: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;
8: ldc #4 // String synchronized...
10: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
13: aload_1
14: monitorexit
15: goto 23
18: astore_2
19: aload_1
20: monitorexit
21: aload_2
22: athrow
23: return
Exception table:
from to target type
5 15 18 any
18 21 18 any
}
Java 對象頭
synchronized
用的鎖是存在 Java 對象頭里的。如果對象是數(shù)組類型瞎饲,則虛擬機用 3 個字寬存儲對象頭口叙,如果對象是非數(shù)組類型,則用 2 個字寬存儲對象頭嗅战。
長度 | 內(nèi)容 | 說明 |
---|---|---|
32/64 bit | Mark Word | 存儲對象的 hashCode 或鎖信息等 |
32/64 bit | Class Metadata Address | 存儲到對象類型數(shù)據(jù)的指針 |
32/6 4bit | Array length | 數(shù)組的長度 |
Java 對象頭里的 Mark Word 里默認存儲對象的 HashCode妄田、分代年齡和鎖標記位。 32 位 JVM 的 Mark Word 的默認存儲結(jié)構(gòu)如下所示:
鎖狀態(tài) | 25 bit | 4 bit | 1 bit 是否是偏向鎖 | 2 bit 鎖標志位 |
---|---|---|---|---|
無鎖狀態(tài) | 對象的 hashCode | 對象分代年齡 | 0 | 01 |
鎖的升級與對比
Java SE 1.6 為了減少獲得鎖和釋放鎖帶來的性能消耗驮捍,引入了 偏向鎖 和 輕量級鎖 疟呐,在 Java SE 1.6 中,鎖一共有 4 種狀態(tài)东且,級別從低到高依次是:無鎖狀態(tài)启具、偏向鎖狀態(tài)、輕量級鎖狀態(tài)和重量級鎖狀態(tài)珊泳,這幾個狀態(tài)會隨著競爭情況逐漸升級鲁冯。鎖可以升級但不能降級囤踩,意味著偏向鎖升級成輕量級鎖之后便不能再降級成偏向鎖。
偏向鎖
JVM 的作者經(jīng)過研究發(fā)現(xiàn)晓褪,大多數(shù)情況下堵漱,鎖不僅不存在多線程競爭,而且總是由同一線程多次獲得涣仿,為了讓線程獲得鎖的代價更低而引入了偏向鎖勤庐。當一個線程訪問同步代碼時,會在對象頭和棧幀中的鎖記錄里存儲鎖偏向的線程 ID好港,以后該線程在進入和退出同步代碼時不需要進行 CAS 操作來加鎖和解鎖愉镰,只需簡單的測試一下對象頭的 Mark Word 里是否存儲著指向當前線程的偏向鎖。如果測試成功钧汹,表示線程已經(jīng)獲得了鎖丈探,如果測試失敗,則需要再測試一下 Mark Word 中偏向鎖的標識是否設置成 1 (表示當前是偏向鎖)拔莱,如果沒有設置碗降,則使用 CAS 競爭鎖;如果設置了塘秦,則嘗試使用 CAS 將對象頭的偏向鎖指向當前線程讼渊。
- 偏向鎖的撤銷
偏向鎖使用了一種等到競爭出現(xiàn)才釋放鎖的機制,所以當其他線程嘗試競爭偏向鎖時尊剔,持有偏向鎖的線程才會釋放鎖爪幻。偏向鎖的撤銷,需要等待全局安全點(在這個時間點上沒有正在執(zhí)行的字節(jié)碼)须误。它會暫停擁有鎖的線程挨稿,然后檢查持有偏向鎖的線程是否活著,如果線程不處于活動狀態(tài)京痢,則將對象頭設置成無鎖狀態(tài)奶甘;如果線程任然活著,擁有偏向鎖的棧會被執(zhí)行历造,遍歷偏向?qū)ο蟮逆i記錄甩十,棧中的鎖記錄和對象頭的 Mark Word 要么重新偏向與其他線程,要么恢復到無鎖活著標記對象不適合作為偏向鎖吭产,最后喚醒暫停的線程侣监。
輕量級鎖
- 輕量級鎖加鎖
線程在執(zhí)行同步代碼塊之前, JVM 會先在當前線程的棧幀中創(chuàng)建用于存儲鎖記錄的空間臣淤,并將對象頭中的 Mark Word 復制到鎖記錄中橄霉,然后線程嘗試使用 CAS 將對象頭中的 Mark Word 替換為指向鎖記錄的指針。如果成功邑蒋,當前線程獲得鎖姓蜂,如果失敗按厘,表示其他線程競爭鎖,當前線程便嘗試使用自旋來獲取鎖钱慢。 - 輕量級鎖解鎖
輕量級鎖解鎖時逮京,會使用原子的 CAS 操作將鎖記錄中的 Mardk Word 替換會到對象頭,如果成功束莫,則表示沒有競爭發(fā)生懒棉。如果失敗,表示當前鎖存在競爭览绿,鎖就會膨脹成重量級鎖策严。
因為自旋會消耗 CPU ,為了避免無用的自旋饿敲,一旦鎖升級成重量級鎖妻导,就不會再恢復到輕量級鎖狀態(tài)。當鎖處于這個狀態(tài)下怀各,其他線程試圖獲取鎖時都會被阻塞住倔韭,當持有鎖的線程釋放鎖之后會喚醒這些線程,被喚醒的線程會進行新一輪的奪鎖之爭渠啤。
鎖的優(yōu)缺點對比
鎖 | 優(yōu)點 | 缺點 | 適用場景 |
---|---|---|---|
偏向鎖 | 加鎖和解鎖不需要額外的消耗狐肢,和執(zhí)行非同步方法相比僅存在納秒級別的差距 | 如果線程間存在鎖競爭添吗,會帶來額外的鎖撤銷的消耗 | 適用于只有一個線程訪問同步塊的場景 |
輕量級鎖 | 競爭的線程不會阻塞沥曹,提高了程序的響應速度 | 如果始終得不到鎖競爭的線程,使用自旋會消耗 CPU | 追求響應時間碟联,同步塊執(zhí)行速度非臣嗣溃快 |
重量級鎖 | 線程競爭不使用自旋,不會消耗 CPU | 線程阻塞鲤孵,響應時間緩慢 | 追求吞吐量壶栋,同步塊執(zhí)行速度較長 |
完