最近在讀Charlie Hunt大神的《Java Performance》娃闲,第三章講《JVM Overview》中間有說到synchronized的一些基本邏輯虚汛。本文會做一些整理,主要內容和重要知識點(本文中若未明確說明皇帮,JVM默認指的是HotSpot版VM):
synchronized是什么
synchronized有哪些常見用法
-
synchronized在JVM中的實現(xiàn)原理
- 同步方法:通過ACC_SYNCHRONIZED標志位來實現(xiàn)
- 同步代碼塊:通過monitorenter和monitorexit命令來實現(xiàn)
-
synchronized使用demo和注意點
- 類對象鎖:修飾靜態(tài)方法和class對象時
- 實例對象鎖:修飾非靜態(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ā)操作機制属拾,用來預防将谊、避免對資源不合適的交替使用(通常競爭),保障交替使用資源的安全渐白。
2. synchronized有哪些常見用法
- 修飾方法
public static synchronized Integer getAgeOne() { //靜態(tài)方法 return age; } public synchronized Integer getAgeTwo() { //實例方法 return age; }
- 修飾代碼塊
public Integer getAgeThree() { synchronized (this) { return age; } }
3. synchronized在HotSpot VM中的實現(xiàn)原理
-
方法
- 通過javap命令反解析class文件尊浓,獲取synchronized在字節(jié)碼層面是如何實現(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文件進行反解析
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 標志位 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 標志位 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關鍵詞的解釋是這樣的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.
翻譯下
同步方法的運行是隱式的纯衍,類似于jvm對于方法的引用和返回的支持栋齿。同步方法通過在運行常量池里method_info數(shù)據(jù)結構中的ACC_SYNCHRONIZED標簽來標注。 如果一個線程發(fā)現(xiàn)調用的方法有ACC_SYNCHRONIZED標記,那么線程的執(zhí)行過程就變成:獲取monitor對象瓦堵,調用方法基协,釋放monitor對象。 在某個線程持有monitor對象時菇用,如果其他線程也想獲取該對象澜驮,則會別阻塞。 如果一個同步方法執(zhí)行過程中發(fā)生異常惋鸥,而且方法自己沒有處理泉唁,那么在異常被向外拋時,線程也會自動釋放monitor對象揩慕。
官方文檔也說的非常清楚了,JVM在處理同步方法時扮休,是通過隱式的獲取monitor對象來實現(xiàn)迎卤。
從反解析的class中也可以看到,同步代碼塊是顯式的通過monitor對象來實現(xiàn)互斥訪問玷坠。
因此可以簡單的歸納下蜗搔,synchronized關鍵詞的實現(xiàn),在JVM中八堡,synchronized通過獲取monitor對象來實現(xiàn)的樟凄。
-
4. synchronized使用demo和注意點
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í)行結果
synchronized static method start !
synchronized static method end 缝龄!
synchronized class start !
synchronized class end !
//在靜態(tài)同步方法執(zhí)行結束后才開始執(zhí)行同步代碼塊
- 分析
- 靜態(tài)方法和同步參數(shù)是class對象時挂谍,執(zhí)行時會獲取class對象的鎖叔壤,所以上述代碼會發(fā)生鎖競爭,執(zhí)行結果也證實了這個邏輯口叙。
- 注意點
- 當你使用synchronized修飾靜態(tài)方法或者class對象時炼绘,要非常謹慎,同一個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() { //同步代碼塊-同步對象為實例對象
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í)行結果
```
first synchronized start !
first synchronized end 萨醒!
synchronized block start !
synchronized block end 斟珊!
second synchronized start !
second synchronized end ! //有序執(zhí)行這3個方法,說明發(fā)生了競爭
```
* 分析
* 實例方法和代碼塊被synchronized修飾時囤踩,執(zhí)行時會獲取實例對象的鎖旨椒,所以上述代碼會發(fā)生鎖競爭,執(zhí)行結果也證實了這個邏輯堵漱。
* 注意點
* 盡量不要用一些公用對象的鎖综慎,比如封裝類常量池中的一些對象: Integer,String等都有類似的邏輯。
5. synchronized鎖優(yōu)化邏輯和鎖升級過程分析
5.1 鎖的幾種狀態(tài)
由于開始設計的同步邏輯勤庐,在發(fā)生互斥資源競爭訪問時示惊,等待的線程會變成block狀態(tài)。而線程的調度是在內核態(tài)運行的愉镰,所以涉及到了內核態(tài)和用戶態(tài)的切換米罚,而且是2次:block時一次,喚醒時一次丈探。
所以這樣的操作效率不高录择,JDK1.6開始,就對synchronized的機制做了優(yōu)化碗降,把鎖的狀態(tài)分成了以下幾種:
- 無鎖狀態(tài):已解鎖
- 偏向鎖:已鎖定/已解鎖且無共享
- 輕量級鎖:已鎖定且共享隘竭,但非競爭。
- 重量級鎖:已鎖定/已解鎖且共享和競爭讼渊。線程在monitor-enter或wait()時被阻塞动看。
5.2 舉個例子來說明這幾種狀態(tài)
- 銀行交易有一個窗口可以辦業(yè)務,門口有個取票機和一個引導員(幫助不會操作的客戶)爪幻。
- 為了每次只有1個客戶到窗口辦業(yè)務菱皆,辦業(yè)務前客戶必須取票,然后系統(tǒng)會根據(jù)取票順序按一定邏輯來叫號挨稿。
- 等待叫號必須去專門的等候區(qū)搔预,等候區(qū)域距離取票機和窗口都有一定的距離。(系統(tǒng)調度效率不高)
- 運行一段時間后叶组,發(fā)現(xiàn)有時候客戶很少拯田,還是需要取號,然后到等候區(qū)等待叫號甩十,如果一個同一個客戶多次辦業(yè)務船庇,就需要來回跑。
優(yōu)化后:
- 在客戶很少的時候侣监,如果窗口空閑鸭轮,則第一個來辦理業(yè)務的人,引導員會只需記錄他的名字橄霉,不用取票窃爷,直接讓他去辦業(yè)務,而且只要沒有新客戶,他多次辦業(yè)務都不需要取票按厘。(這時候變成了偏向鎖)
- 正在他享受這超級vip服務的時候医吊,又來了新的客戶,新客戶也知道銀行的新規(guī)定逮京,沒有直接取號卿堂,而是詢問引導員是否可辦業(yè)務,引導員說不行懒棉,因為現(xiàn)在有人在辦草描。(這時候變成了輕量級鎖,通過cas判斷是否能獲取鎖)
- 新客戶知道取票等候區(qū)一套流程蠻麻煩策严,所以告訴引導員說穗慕,他可以旁邊等一等靖榕,前面人辦完了索昂,他也想直接進去辦業(yè)務嫉晶。(這時候變成了自旋鎖)
- 新客戶發(fā)現(xiàn)自己詢問了10次都沒等到辦業(yè)務铐然,所以直接向銀行大堂經理投訴。銀行經理就過來說耿导,今天你們不準不取號了,每次進去辦業(yè)務必須取號。(這時候變成了重鎖)
分析:
- 偏向鎖適合的場景是狐肢,某段時間內訪問互斥資源的線程基本是同一個,沒有共享訪問的場景
- 輕量級鎖適合的場景是沥曹,每次訪問互斥資源的時間很短份名,大家能共享訪問,互不影響
- 重量級鎖適合的場景是妓美,常發(fā)生競爭僵腺,每次占用資源的時間都不短
5.3鎖升級簡化版
- Mark Word介紹
-
JVM主要通過對象頭中的Mark Word來標記鎖的相關狀態(tài),包括當前鎖的狀態(tài)和持有鎖對象的信息壶栋,下面是在不同狀態(tài)下Mark Word的信息辰如。
-
- 鎖升級流程簡化版
-
很多博客中有一個詳細版的鎖升級流程,我把他們簡化了下贵试,更容易理解一些
-
- 注意點
- 鎖的狀態(tài)只有4種琉兜,無鎖->偏向鎖->輕量級鎖->重量級鎖
- 升級過程不可逆,不同階段通過從輕到重的方式獲取鎖
- 自旋這個操作是通過線程死循環(huán)毙玻,而防止被阻塞豌蟋,試圖避免用戶態(tài)和內核態(tài)的切換,所以本身不屬于鎖的狀態(tài)桑滩,是配合輕量級鎖使用的一種方式
本文中所有的代碼和說明都可以在github中找到梧疲,戳這里>
我是大旗,努力用易理解的案例分析進階知識,一起來學習JVM調優(yōu)幌氮,高并發(fā)缭受,常用中間件吧~
如果喜歡我的文章, 來關注我吧~ [岳大旗的博客]