synchronized是什麼
- 關鍵字,Java利用鎖機制實現線程同步的一種方式。
- Java實現線程同步的方式:
1.顯式鎖(lock,需要自己寫代碼去獲取鎖和釋放鎖);
2.隱式鎖(synchronized,自動的)。
synchronized的保證的特性
- 原子性:被synchronized關鍵字包裹起來的方法或者代碼塊可以認爲是原子的。因爲在鎖未釋放之前,這段代碼無法被其他線程訪問到,所以從一個線程觀察另外一個線程的時候,看到的都是一個個原子性的操作。在Java中,synchronized對應着兩個字節碼指令monitorenter和monitorexit。通過monitorenter和monitorexit指令,可以保證被synchronized修飾的代碼在同一時間只能被一個線程訪問,在鎖未釋放之前,無法被其他線程訪問到。
- 可見性:根據JMM(Java Memory Model,Java內存模型)機制,內存主要分爲主內存和工作內存兩種,線程工作時會從主內存中拷貝一份變量到工作內存中。
JMM對synchronized做了2條規定:
1.線程解鎖前,必須把變量的最新值刷新到主內存中。
2.線程加鎖時,先清空工作內存中的變量值,從主內存中重新獲取最新值到工作內存中。 - 有序性:synchronized可以保證一定程度的有序性,但其是不能禁止指令重排序的,synchronized 代碼塊裏的非原子操作依舊可能發生指令重排。
這裏要先說一個概念,as-if-serial語義,其是指不管怎麼重排序(編譯器和處理器爲了提高並行度),單線程程序的執行結果都不能被改變。編譯器和處理器無論如何優化,都必須遵守as-if-serial語義。
as-if-serial語義保證了單線程中指令重排序是有一些限制的,即無論怎麼重排序,都不能影響到單線程執行的結果。而synchronized保證了這一塊程序在同一時間內只能被同一線程訪問,所以其也算是保證了有序性。
synchronized的作用
它的作用有三點:
- 確保線程互斥的訪問同步代碼
- 保證共享爲師的修改及時可見
- 有效解決指令重排,但是不能禁止(synchronized同步中的代碼,JVM不會輕易優化重排序)
synchronized怎麼用
- 修飾方法
//普通方法
//鎖對象
public synchronized void synTest (){
//TODO
}
//靜態方法
//鎖類
public class SynClass {
public static synchronized void show (){}
}
- 修飾代碼塊
public class SynClass {
//鎖類
public static void main(String[] args) {
synchronized(SynClass.class){
//TODO
}
}
}
public class SynClass {
//鎖對象
Object lock = new Object();
public void show(){
synchronized(lock){
//TODO
}
}
}
有兩種加鎖方式:
- 對象鎖
- 類鎖
synchronized是怎麼實現的
- Java虛擬機中的同步是基於監視器(Monitor)對象實現的,待會細說。瞭解一個概念先
Java對象頭
在JVM中對象在內存中的佈局分三塊:對象頭、實例變量、對齊填充。
- 對象頭是實現synchronized的鎖對象的基礎
- 主要結構是由Mark Word 和 Class Metadata Address 組成
Mark Word默認存儲結構外,還有如下可能變化的結構:
monitor對象存在於每個Java對象的對象頭中(存儲的指針的指向),synchronized鎖便是通過這種方式獲取鎖的,也是爲什麼Java中任意對象可以作爲鎖的原因。
synchronized代碼塊底層原理
- 定義一個synchronized修飾的同步代碼塊,在代碼塊中操作共享變量i
public class SyncCodeBlock {
public int i;
public void syncTask(){
//同步代碼庫
synchronized (this){
i++;
}
}
}
編譯上述代碼並使用javap反編譯後得到字節碼如下(這裏我們省略一部分沒有必要的信息):
Classfile /Users/zejian/Downloads/Java8_Action/src/main/java/com/zejian/concurrencys/SyncCodeBlock.class
Last modified 2020-5-5; size 426 bytes
MD5 checksum c80bc322c87b312de760942820b4fed5
Compiled from "SyncCodeBlock.java"
public class com.zejian.concurrencys.SyncCodeBlock
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
//........省略常量池中數據
//構造函數
public com.zejian.concurrencys.SyncCodeBlock();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 7: 0
//===========主要看看syncTask方法實現================
public void syncTask();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=3, locals=3, args_size=1
0: aload_0
1: dup
2: astore_1
3: monitorenter //注意此處,進入同步方法
4: aload_0
5: dup
6: getfield #2 // Field i:I
9: iconst_1
10: iadd
11: putfield #2 // Field i:I
14: aload_1
15: monitorexit //注意此處,退出同步方法
16: goto 24
19: astore_2
20: aload_1
21: monitorexit //注意此處,退出同步方法
22: aload_2
23: athrow
24: return
Exception table:
//省略其他字節碼.......
}
SourceFile: "SyncCodeBlock.java"
主要看字節碼中的如下代碼:
3: monitorenter //進入同步方法
//..........省略其他
15: monitorexit //退出同步方法
16: goto 24
//省略其他.......
21: monitorexit //退出同步方法
- monitorenter指令指向同步代碼塊的開始位置
- monitorexit指令則指明同步代碼塊的結束位置
- 當執行monitorenter指令時,當前線程試圖獲取 objectref(即對象鎖) 所對應的 monitor 的持有權,當 objectref 的 monitor 的進入計數器爲 0,那線程可以成功取得 monitor,並將計數器值設置爲 1,取鎖成功。
- 如果當前線程已經擁有 objectref 的 monitor 的持有權,那它可以重入這個 monitor,重入時計數器的值也會加 1。
- 如果其他線程已經擁有 objectref 的 monitor 的所有權,那麼當前線程將被阻塞,直到正在執行線程執行完畢,即monitorexit指令被執行,執行線程將釋放 monitor(鎖)並設置計數器值爲0 。
synchronized方法底層原理
方法級的同步是隱式,即無需通過字節碼指令來控制的,它實現在方法調用和返回操作之中。JVM可以從方法常量池中的方法表結構(method_info Structure) 中的 ACC_SYNCHRONIZED 訪問標誌區分一個方法是否同步方法。當方法調用時,調用指令將會 檢查方法的 ACC_SYNCHRONIZED 訪問標誌是否被設置,如果設置了,執行線程將先持有monitor(虛擬機規範中用的是管程一詞), 然後再執行方法,最後再方法完成(無論是正常完成還是非正常完成)時釋放monitor。在方法執行期間,執行線程持有了monitor,其他任何線程都無法再獲得同一個monitor。如果一個同步方法執行期間拋 出了異常,並且在方法內部無法處理此異常,那這個同步方法所持有的monitor將在異常拋到同步方法之外時自動釋放。下面我們看看字節碼層面如何實現:
public class SyncMethod {
public int i;
public synchronized void syncTask(){
i++;
}
}
用javap反編譯後的字節碼:
Classfile /Users/zejian/Downloads/Java8_Action/src/main/java/com/zejian/concurrencys/SyncMethod.class
Last modified 2020-5-5; size 308 bytes
MD5 checksum f34075a8c059ea65e4cc2fa610e0cd94
Compiled from "SyncMethod.java"
public class com.zejian.concurrencys.SyncMethod
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool;
//省略沒必要的字節碼
//==================syncTask方法======================
public synchronized void syncTask();
descriptor: ()V
//方法標識ACC_PUBLIC代表public修飾,ACC_SYNCHRONIZED指明該方法爲同步方法
flags: ACC_PUBLIC, ACC_SYNCHRONIZED
Code:
stack=3, locals=1, args_size=1
0: aload_0
1: dup
2: getfield #2 // Field i:I
5: iconst_1
6: iadd
7: putfield #2 // Field i:I
10: return
LineNumberTable:
line 12: 0
line 13: 10
}
SourceFile: "SyncMethod.java"
- synchronized修飾的方法並沒有monitorenter指令和monitorexit指令。
- 取得代之的是ACC_SYNCHRONIZED標識,該標識指明瞭該方法是一個同步方法。
- JVM通過該ACC_SYNCHRONIZED訪問標誌來辨別一個方法是否聲明爲同步方法,從而執行相應的同步調用。這便是synchronized鎖在同步代碼塊和同步方法上實現的基本原理。
- 在Java早期版本中,synchronized屬於重量級鎖,效率低下,因爲監視器鎖(monitor)是依賴於底層的操作系統的Mutex Lock來實現的,而操作系統實現線程之間的切換時需要從用戶態轉換到核心態,這個狀態之間的轉換需要相對比較長的時間,時間成本相對較高,這也是爲什麼早期的synchronized效率低的原因。
- 在Java 6之後Java官方對從JVM層面對synchronized較大優化,所以現在的synchronized鎖效率也優化得很不錯了,Java 6之後,爲了減少獲得鎖和釋放鎖所帶來的性能消耗,引入了輕量級鎖和偏向鎖.