synchronized 關鍵字底層原理屬於 JVM 層面。原理如下:
① synchronized 同步語句塊的情況
public class SynchronizedDemo { public void method() { synchronized (this) { System.out.println("synchronized 代碼塊"); } } }
編譯後生成字節碼文件通過執行javap -c -s -v -l SynchronizedDemo.class
。
從上面我們可以看出:
synchronized 同步語句塊的實現使用的是 monitorenter 和 monitorexit 指令,其中 monitorenter 指令指向同步代碼塊的開始位置,monitorexit 指令則指明同步代碼塊的結束位置。 當執行 monitorenter 指令時,線程試圖獲取鎖也就是獲取 monitor(monitor對象存在於每個Java對象的對象頭中,synchronized 鎖便是通過這種方式獲取鎖的,也是爲什麼Java中任意對象可以作爲鎖的原因) 的持有權。當計數器爲0則可以成功獲取,獲取後將鎖計數器設爲1也就是加1。相應的在執行 monitorexit 指令後,將鎖計數器設爲0,表明鎖被釋放。如果獲取對象鎖失敗,那當前線程就要阻塞等待,直到鎖被另外一個線程釋放爲止。
② synchronized 修飾方法的的情況
public class SynchronizedDemo2 { public synchronized void method() { System.out.println("synchronized 方法"); } }
synchronized 修飾的方法並沒有 monitorenter 指令和 monitorexit 指令,取得代之的確實是 ACC_SYNCHRONIZED 標識,該標識指明瞭該方法是一個同步方法,JVM 通過該 ACC_SYNCHRONIZED 訪問標誌來辨別一個方法是否聲明爲同步方法,從而執行相應的同步調用。
使用方法:
synchronized關鍵字最主要的三種使用方式:
-
修飾實例方法: 作用於當前對象實例加鎖,進入同步代碼前要獲得當前對象實例的鎖
-
修飾靜態方法: :也就是給當前類加鎖,會作用於類的所有對象實例,因爲靜態成員不屬於任何一個實例對象,是類成員( static 表明這是該類的一個靜態資源,不管new了多少個對象,只有一份)。所以如果一個線程A調用一個實例對象的非靜態 synchronized 方法,而線程B需要調用這個實例對象所屬類的靜態 synchronized 方法,是允許的,不會發生互斥現象,因爲訪問靜態 synchronized 方法佔用的鎖是當前類的鎖,而訪問非靜態 synchronized 方法佔用的鎖是當前實例對象鎖。
-
修飾代碼塊: 指定加鎖對象,對給定對象加鎖,進入同步代碼庫前要獲得給定對象的鎖。
總結: synchronized 關鍵字加到 static 靜態方法和 synchronized(class)代碼塊上都是是給 Class 類上鎖。synchronized 關鍵字加到實例方法上是給對象實例上鎖。儘量不要使用 synchronized(String a) 因爲JVM中,字符串常量池具有緩存功能!
使用:
public class SingletonDemo3 { /* 雙重鎖鑑定 */ private SingletonDemo3() { } private volatile static SingletonDemo3 instance; public SingletonDemo3 getInstance() { if (instance == null) { synchronized (SingletonDemo3.class) { if (instance == null) { //volatile保證可見性,禁止指令重排序 instance = new SingletonDemo3(); } } } return instance; } public static void main(String[] args) { SingletonDemo3 s = new SingletonDemo3(); System.out.println(s.getInstance()); } }
instance 採用 volatile 關鍵字修飾也是很有必要的,
instance = new SingletonDemo3(); 這段代碼其實是分爲三步執行:
-
爲 instance 分配內存空間
-
初始化 instance
-
將 instance指向分配的內存地址
但是由於 JVM 具有指令重排的特性,執行順序有可能變成 1->3->2。指令重排在單線程環境下不會出先問題,但是在多線程環境下會導致一個線程獲得還沒有初始化的實例。例如,線程 T1 執行了 1 和 3,此時 T2 調用 getInstance() 後發現 instance不爲空,因此返回 instance,但此時 instance還未被初始化。
使用 volatile 可以禁止 JVM 的指令重排,保證在多線程環境下也能正常運行。