最近在讀Charlie Hunt大神的《Java Performance》,第三章講《JVM Overview》中間有說到synchronized的一些基本邏輯。本文會做一些整理,主要內(nèi)容和重要知識點(diǎn)(本文中若未明確說明,JVM默認(rèn)指的是HotSpot版VM):
synchronized是什么
synchronized有哪些常見用法
-
synchronized在JVM中的實(shí)現(xiàn)原理
- 同步方法:通過ACC_SYNCHRONIZED標(biāo)志位來實(shí)現(xiàn)
- 同步代碼塊:通過monitorenter和monitorexit命令來實(shí)現(xiàn)
-
synchronized使用demo和注意點(diǎn)
- 類對象鎖:修飾靜態(tài)方法和class對象時
- 實(shí)例對象鎖:修飾非靜態(tài)方法、代碼塊和非class對象時
-
synchronized鎖優(yōu)化和鎖升級過程
- 無鎖
- 偏向鎖
- 輕量級鎖
- 重量級鎖
1. synchronized是什么?
《Java performance》中的定義是:
Synchronization is described as a mechanism that prevents, avoids,
or recovers from the inopportune interleavings, commonly called races, of concurrent operations.
翻譯:
同步是一種并發(fā)操作機(jī)制,用來預(yù)防、避免對資源不合適的交替使用(通常競爭),保障交替使用資源的安全。
2. synchronized有哪些常見用法
- 修飾方法
public static synchronized Integer getAgeOne() { //靜態(tài)方法 return age; } public synchronized Integer getAgeTwo() { //實(shí)例方法 return age; } - 修飾代碼塊
public Integer getAgeThree() { synchronized (this) { return age; } }
3. synchronized在HotSpot VM中的實(shí)現(xiàn)原理
-
方法
- 通過javap命令反解析class文件,獲取synchronized在字節(jié)碼層面是如何實(shí)現(xiàn)的。
-
步驟
- 創(chuàng)建一個demo類
public class SynchronizedDemoOne { private static int age = 1; /** * synchronized 修飾靜態(tài)方法 */ public static synchronized Integer getAgeOne() { return age; } /** * synchronized 修飾非靜態(tài)方法 */ public synchronized Integer getAgeTwo() { return age; } /** * synchronized 修飾代碼塊完整 */ public Integer getAgeThree() { synchronized (this) { return age; } } }- 通過classc命令把java編譯成class文件
javac -g ./SynchronizedDemoOne.java- 通過classp命令對class文件進(jìn)行反解析
javap -verbose SynchronizedDemoOne- 得到反解析后的文件
Classfile /Users/height/git/learn/JavaAccumulator/src/com/height/concurrent/synchronization/implementation/SynchronizedDemoOne.class Last modified 2020-9-9; size 877 bytes MD5 checksum bdd02e83e30f0ac316a408694f638868 Compiled from "SynchronizedDemoOne.java" public class com.height.concurrent.synchronization.implementation.SynchronizedDemoOne minor version: 0 major version: 52 flags: ACC_PUBLIC, ACC_SUPER Constant pool: #1 = Methodref #5.#26 #2 = Fieldref #4.#27 . //中間省略部分 . . #34 = Utf8 valueOf #35 = Utf8 (I)Ljava/lang/Integer; { public com.height.concurrent.synchronization.implementation.SynchronizedDemoOne(); descriptor: ()V flags: ACC_PUBLIC Code: stack=1, locals=1, args_size=1 0: aload_0 1: invokespecial #1 4: return LineNumberTable: line 3: 0 LocalVariableTable: Start Length Slot Name Signature 0 5 0 this Lcom/height/concurrent/synchronization/implementation/SynchronizedDemoOne; public static synchronized java.lang.Integer getAgeOne(); descriptor: ()Ljava/lang/Integer; flags: ACC_PUBLIC, ACC_STATIC, ACC_SYNCHRONIZED // ACC_SYNCHRONIZED 標(biāo)志位 Code: stack=1, locals=0, args_size=0 0: getstatic #2 3: invokestatic #3 6: areturn LineNumberTable: line 8: 0 public synchronized java.lang.Integer getAgeTwo(); descriptor: ()Ljava/lang/Integer; flags: ACC_PUBLIC, ACC_SYNCHRONIZED // ACC_SYNCHRONIZED 標(biāo)志位 Code: stack=1, locals=1, args_size=1 0: getstatic #2 3: invokestatic #3 6: areturn LineNumberTable: line 12: 0 LocalVariableTable: Start Length Slot Name Signature 0 7 0 this Lcom/height/concurrent/synchronization/implementation/SynchronizedDemoOne; public java.lang.Integer getAgeThree(); descriptor: ()Ljava/lang/Integer; flags: ACC_PUBLIC Code: stack=2, locals=3, args_size=1 0: aload_0 1: dup 2: astore_1 3: monitorenter //獲取monitor對象 4: getstatic #2 7: invokestatic #3 10: aload_1 11: monitorexit //釋放monitor對象 12: areturn 13: astore_2 14: aload_1 15: monitorexit //鎖定過程中發(fā)生異常時的釋放monitor對象 16: aload_2 17: athrow Exception table: from to target type 4 12 13 any 13 16 13 any LineNumberTable: line 16: 0 line 17: 4 line 18: 13 LocalVariableTable: Start Length Slot Name Signature 0 18 0 this Lcom/height/concurrent/synchronization/implementation/SynchronizedDemoOne; StackMapTable: number_of_entries = 1 frame_type = 255 /* full_frame */ offset_delta = 13 locals = [ class com/height/concurrent/synchronization/implementation/SynchronizedDemoOne, class java/lang/Object ] stack = [ class java/lang/Throwable ] . . //省略部分 . }的文件參見: 反解析完整文件
-
分析
-
官方對synchronized關(guān)鍵詞的解釋是這樣的synchronized官方解釋
Method-level synchronization is performed implicitly, as part of method invocation and return. A synchronized method is distinguished in the run-time constant pool’s method_info structure by the ACC_SYNCHRONIZED flag, which is checked by the method invocation instructions. When invoking a method for which ACC_SYNCHRONIZED is set, the executing thread enters a monitor, invokes the method itself, and exits the monitor whether the method invocation completes normally or abruptly. During the time the executing thread owns the monitor, no other thread may enter it. If an exception is thrown during invocation of the synchronized method and the synchronized method does not handle the exception, the monitor for the method is automatically exited before the exception is rethrown out of the synchronized method.翻譯下
同步方法的運(yùn)行是隱式的,類似于jvm對于方法的引用和返回的支持。同步方法通過在運(yùn)行常量池里method_info數(shù)據(jù)結(jié)構(gòu)中的ACC_SYNCHRONIZED標(biāo)簽來標(biāo)注。 如果一個線程發(fā)現(xiàn)調(diào)用的方法有ACC_SYNCHRONIZED標(biāo)記,那么線程的執(zhí)行過程就變成:獲取monitor對象,調(diào)用方法,釋放monitor對象。 在某個線程持有monitor對象時,如果其他線程也想獲取該對象,則會別阻塞。 如果一個同步方法執(zhí)行過程中發(fā)生異常,而且方法自己沒有處理,那么在異常被向外拋時,線程也會自動釋放monitor對象。 官方文檔也說的非常清楚了,JVM在處理同步方法時,是通過隱式的獲取monitor對象來實(shí)現(xiàn)。
從反解析的class中也可以看到,同步代碼塊是顯式的通過monitor對象來實(shí)現(xiàn)互斥訪問。
因此可以簡單的歸納下,synchronized關(guān)鍵詞的實(shí)現(xiàn),在JVM中,synchronized通過獲取monitor對象來實(shí)現(xiàn)的。
-
4. synchronized使用demo和注意點(diǎn)
4.1 案例1
- 代碼
public class SynchronizedDemoTwo {
public synchronized static void synchronizedStaticMethodMethod() { //同步靜態(tài)方法
System.out.println("synchronized static method start !");
sleep(1000);
System.out.println("synchronized static method end !");
}
public static void synchronizedClassMethod() { //同步代碼塊-同步對象為class對象
synchronized (SynchronizedDemoTwo.class) {
System.out.println("synchronized class start !");
sleep(1000);
System.out.println("synchronized class end !");
}
}
public static void main(String args[]) {
synchronizedRun();
}
private static void synchronizedRun() {
new Thread(new Runnable() {
@Override
public void run() {
SynchronizedDemoTwo.synchronizedStaticMethodMethod();
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
SynchronizedDemoTwo.synchronizedClassMethod();
}
}).start();
}
private static void sleep(int second) {
try {
Thread.sleep(second);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
- 執(zhí)行結(jié)果
synchronized static method start !
synchronized static method end !
synchronized class start !
synchronized class end !
//在靜態(tài)同步方法執(zhí)行結(jié)束后才開始執(zhí)行同步代碼塊
- 分析
- 靜態(tài)方法和同步參數(shù)是class對象時,執(zhí)行時會獲取class對象的鎖,所以上述代碼會發(fā)生鎖競爭,執(zhí)行結(jié)果也證實(shí)了這個邏輯。
- 注意點(diǎn)
- 當(dāng)你使用synchronized修飾靜態(tài)方法或者class對象時,要非常謹(jǐn)慎,同一個class只有一把鎖,這個鎖作用域是非常大的。像String.class,Integer.class這些原生類也不要輕易加鎖。
4.2 案例2
- 代碼
public class SynchronizedDemoThree {
public synchronized void firstSynchronizedMethod() { //同步方法1
System.out.println("first synchronized start !");
sleep(1000);
System.out.println("first synchronized end !");
}
public synchronized void secondSynchronizedMethod() { //同步方法2
System.out.println("second synchronized start !");
sleep(1000);
System.out.println("second synchronized end !");
}
public void synchronizedBlockMethod() { //同步代碼塊-同步對象為實(shí)例對象
synchronized (this) {
System.out.println("synchronized block start !");
sleep(1000);
System.out.println("synchronized block end !");
}
}
public static void main(String args[]) {
SynchronizedDemoThree demo1 = new SynchronizedDemoThree();
new Thread(new Runnable() {
@Override
public void run() {
demo1.firstSynchronizedMethod();
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
demo1.secondSynchronizedMethod();
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
demo1.synchronizedBlockMethod();
}
}).start();
}
private static void sleep(int second) {
try {
Thread.sleep(second);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
- 執(zhí)行結(jié)果
```
first synchronized start !
first synchronized end !
synchronized block start !
synchronized block end !
second synchronized start !
second synchronized end ! //有序執(zhí)行這3個方法,說明發(fā)生了競爭
```
* 分析
* 實(shí)例方法和代碼塊被synchronized修飾時,執(zhí)行時會獲取實(shí)例對象的鎖,所以上述代碼會發(fā)生鎖競爭,執(zhí)行結(jié)果也證實(shí)了這個邏輯。
* 注意點(diǎn)
* 盡量不要用一些公用對象的鎖,比如封裝類常量池中的一些對象: Integer,String等都有類似的邏輯。
5. synchronized鎖優(yōu)化邏輯和鎖升級過程分析
5.1 鎖的幾種狀態(tài)
由于開始設(shè)計(jì)的同步邏輯,在發(fā)生互斥資源競爭訪問時,等待的線程會變成block狀態(tài)。而線程的調(diào)度是在內(nèi)核態(tài)運(yùn)行的,所以涉及到了內(nèi)核態(tài)和用戶態(tài)的切換,而且是2次:block時一次,喚醒時一次。
所以這樣的操作效率不高,JDK1.6開始,就對synchronized的機(jī)制做了優(yōu)化,把鎖的狀態(tài)分成了以下幾種:
- 無鎖狀態(tài):已解鎖
- 偏向鎖:已鎖定/已解鎖且無共享
- 輕量級鎖:已鎖定且共享,但非競爭。
- 重量級鎖:已鎖定/已解鎖且共享和競爭。線程在monitor-enter或wait()時被阻塞。
5.2 舉個例子來說明這幾種狀態(tài)
- 銀行交易有一個窗口可以辦業(yè)務(wù),門口有個取票機(jī)和一個引導(dǎo)員(幫助不會操作的客戶)。
- 為了每次只有1個客戶到窗口辦業(yè)務(wù),辦業(yè)務(wù)前客戶必須取票,然后系統(tǒng)會根據(jù)取票順序按一定邏輯來叫號。
- 等待叫號必須去專門的等候區(qū),等候區(qū)域距離取票機(jī)和窗口都有一定的距離。(系統(tǒng)調(diào)度效率不高)
- 運(yùn)行一段時間后,發(fā)現(xiàn)有時候客戶很少,還是需要取號,然后到等候區(qū)等待叫號,如果一個同一個客戶多次辦業(yè)務(wù),就需要來回跑。
優(yōu)化后:
- 在客戶很少的時候,如果窗口空閑,則第一個來辦理業(yè)務(wù)的人,引導(dǎo)員會只需記錄他的名字,不用取票,直接讓他去辦業(yè)務(wù),而且只要沒有新客戶,他多次辦業(yè)務(wù)都不需要取票。(這時候變成了偏向鎖)
- 正在他享受這超級vip服務(wù)的時候,又來了新的客戶,新客戶也知道銀行的新規(guī)定,沒有直接取號,而是詢問引導(dǎo)員是否可辦業(yè)務(wù),引導(dǎo)員說不行,因?yàn)楝F(xiàn)在有人在辦。(這時候變成了輕量級鎖,通過cas判斷是否能獲取鎖)
- 新客戶知道取票等候區(qū)一套流程蠻麻煩,所以告訴引導(dǎo)員說,他可以旁邊等一等,前面人辦完了,他也想直接進(jìn)去辦業(yè)務(wù)。(這時候變成了自旋鎖)
- 新客戶發(fā)現(xiàn)自己詢問了10次都沒等到辦業(yè)務(wù),所以直接向銀行大堂經(jīng)理投訴。銀行經(jīng)理就過來說,今天你們不準(zhǔn)不取號了,每次進(jìn)去辦業(yè)務(wù)必須取號。(這時候變成了重鎖)
分析:
- 偏向鎖適合的場景是,某段時間內(nèi)訪問互斥資源的線程基本是同一個,沒有共享訪問的場景
- 輕量級鎖適合的場景是,每次訪問互斥資源的時間很短,大家能共享訪問,互不影響
- 重量級鎖適合的場景是,常發(fā)生競爭,每次占用資源的時間都不短
5.3鎖升級簡化版
- Mark Word介紹
-
JVM主要通過對象頭中的Mark Word來標(biāo)記鎖的相關(guān)狀態(tài),包括當(dāng)前鎖的狀態(tài)和持有鎖對象的信息,下面是在不同狀態(tài)下Mark Word的信息。
mark word.png
-
- 鎖升級流程簡化版
-
很多博客中有一個詳細(xì)版的鎖升級流程,我把他們簡化了下,更容易理解一些
鎖升級.png
-
- 注意點(diǎn)
- 鎖的狀態(tài)只有4種,無鎖->偏向鎖->輕量級鎖->重量級鎖
- 升級過程不可逆,不同階段通過從輕到重的方式獲取鎖
- 自旋這個操作是通過線程死循環(huán),而防止被阻塞,試圖避免用戶態(tài)和內(nèi)核態(tài)的切換,所以本身不屬于鎖的狀態(tài),是配合輕量級鎖使用的一種方式
本文中所有的代碼和說明都可以在github中找到,戳這里>
我是大旗,努力用易理解的案例分析進(jìn)階知識,一起來學(xué)習(xí)JVM調(diào)優(yōu),高并發(fā),常用中間件吧~
如果喜歡我的文章, 來關(guān)注我吧~ [岳大旗的博客]

