天天用Synchronized,底層原理是個啥?
- 2019 年 10 月 31 日
- 筆記
Synchronized 的基本使用
Synchronized 的作用主要有三個:
確保執行緒互斥的訪問同步程式碼
保證共享變數的修改能夠及時可見
有效解決重排序問題
從語法上講,Synchronized 總共有三種用法:
修飾普通方法
修飾靜態方法
修飾程式碼塊
接下來我就通過幾個例子程式來說明一下這三種使用方式(為了便於比較,三段程式碼除了 Synchronized 的使用方式不同以外,其他基本保持一致)。
沒有同步的情況
程式碼段 1:
package com.paddx.test.concurrent; public class SynchronizedTest { public void method1(){ System.out.println("Method 1 start"); try { System.out.println("Method 1 execute"); Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("Method 1 end"); } public void method2(){ System.out.println("Method 2 start"); try { System.out.println("Method 2 execute"); Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("Method 2 end"); } public static void main(String[] args) { final SynchronizedTest test = new SynchronizedTest(); new Thread(new Runnable() { @Override public void run() { test.method1(); } }).start(); new Thread(new Runnable() { @Override public void run() { test.method2(); } }).start(); } }
執行結果如下,執行緒 1 和執行緒 2 同時進入執行狀態,執行緒 2 執行速度比執行緒 1 快,所以執行緒 2 先執行完成。推薦閱讀:多執行緒 start 和 run 方法到底有什麼區別?
這個過程中執行緒 1 和執行緒 2 是同時執行的:
Method 1 start Method 1 execute Method 2 start Method 2 execute Method 2 end Method 1 end
對普通方法同步
程式碼段 2:
package com.paddx.test.concurrent; public class SynchronizedTest { public synchronized void method1(){ System.out.println("Method 1 start"); try { System.out.println("Method 1 execute"); Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("Method 1 end"); } public synchronized void method2(){ System.out.println("Method 2 start"); try { System.out.println("Method 2 execute"); Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("Method 2 end"); } public static void main(String[] args) { final SynchronizedTest test = new SynchronizedTest(); new Thread(new Runnable() { @Override public void run() { test.method1(); } }).start(); new Thread(new Runnable() { @Override public void run() { test.method2(); } }).start(); } }
執行結果如下,跟程式碼段 1 比較,可以很明顯的看出,執行緒 2 需要等待執行緒 1 的 Method1 執行完成才能開始執行 Method2 方法。
Method 1 start Method 1 execute Method 1 end Method 2 start Method 2 execute Method 2 end
靜態方法(類)同步
程式碼段 3:
package com.paddx.test.concurrent; public class SynchronizedTest { public static synchronized void method1(){ System.out.println("Method 1 start"); try { System.out.println("Method 1 execute"); Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("Method 1 end"); } public static synchronized void method2(){ System.out.println("Method 2 start"); try { System.out.println("Method 2 execute"); Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("Method 2 end"); } public static void main(String[] args) { final SynchronizedTest test = new SynchronizedTest(); final SynchronizedTest test2 = new SynchronizedTest(); new Thread(new Runnable() { @Override public void run() { test.method1(); } }).start(); new Thread(new Runnable() { @Override public void run() { test2.method2(); } }).start(); } }
執行結果如下,對靜態方法的同步本質上是對類的同步(靜態方法本質上是屬於類的方法,而不是對象上的方法)。
所以即使 Test 和 Test2 屬於不同的對象,但是它們都屬於 SynchronizedTest 類的實例。
所以也只能順序的執行 Method1 和 Method2,不能並發執行:
Method 1 start Method 1 execute Method 1 end Method 2 start Method 2 execute Method 2 end
程式碼塊同步
程式碼段 4:
package com.paddx.test.concurrent; public class SynchronizedTest { public void method1(){ System.out.println("Method 1 start"); try { synchronized (this) { System.out.println("Method 1 execute"); Thread.sleep(3000); } } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("Method 1 end"); } public void method2(){ System.out.println("Method 2 start"); try { synchronized (this) { System.out.println("Method 2 execute"); Thread.sleep(1000); } } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("Method 2 end"); } public static void main(String[] args) { final SynchronizedTest test = new SynchronizedTest(); new Thread(new Runnable() { @Override public void run() { test.method1(); } }).start(); new Thread(new Runnable() { @Override public void run() { test.method2(); } }).start(); } }
執行結果如下,雖然執行緒 1 和執行緒 2 都進入了對應的方法開始執行,但是執行緒 2 在進入同步塊之前,需要等待執行緒 1 中同步塊執行完成。
Method 1 start Method 1 execute Method 2 start Method 1 end Method 2 execute Method 2 end
Synchronized 原理
如果對上面的執行結果還有疑問,也先不用急,我們先來了解 Synchronized 的原理。推薦閱讀:面試常考:Synchronized 有幾種用法?
再回頭上面的問題就一目了然了。我們先通過反編譯下面的程式碼來看看 Synchronized 是如何實現對程式碼塊進行同步的:
package com.paddx.test.concurrent; public class SynchronizedMethod { public synchronized void method() { System.out.println("Hello World!"); } }
反編譯結果:

關於這兩條指令的作用,我們直接參考 JVM 規範中描述:
monitorenter :Each object is associated with a monitor. A monitor is locked if and only if it has an owner. The thread that executes monitorenter attempts to gain ownership of the monitor associated with objectref, as follows:
• If the entry count of the monitor associated with objectref is zero, the thread enters the monitor and sets its entry count to one. The thread is then the owner of the monitor.
• If the thread already owns the monitor associated with objectref, it reenters the monitor, incrementing its entry count.
• If another thread already owns the monitor associated with objectref, the thread blocks until the monitor's entry count is zero, then tries again to gain ownership.
這段話的大概意思為:每個對象有一個監視器鎖(Monitor),當 Monitor 被佔用時就會處於鎖定狀態。
執行緒執行 Monitorenter 指令時嘗試獲取 Monitor 的所有權,過程如下:
如果 Monitor 的進入數為 0,則該執行緒進入 Monitor,然後將進入數設置為 1,該執行緒即為 Monitor 的所有者。
如果執行緒已經佔有該 Monitor,只是重新進入,則進入 Monitor 的進入數加 1。
如果其他執行緒已經佔用了 Monitor,則該執行緒進入阻塞狀態,直到 Monitor 的進入數為 0,再重新嘗試獲取 Monitor 的所有權。
monitorexit:The thread that executes monitorexit must be the owner of the monitor associated with the instance referenced by objectref.
The thread decrements the entry count of the monitor associated with objectref. If as a result the value of the entry count is zero, the thread exits the monitor and is no longer its owner.
Other threads that are blocking to enter the monitor are allowed to attempt to do so.
這段話的大概意思為:執行 Monitorexit 的執行緒必須是 Objectref 所對應的 Monitor 的所有者。
指令執行時,Monitor 的進入數減 1,如果減 1 後進入數為 0,那執行緒退出 Monitor,不再是這個 Monitor 的所有者。
其他被這個 Monitor 阻塞的執行緒可以嘗試去獲取這個 Monitor 的所有權。
通過這兩段描述,我們應該能很清楚的看出 Synchronized 的實現原理。
Synchronized 的語義底層是通過一個 Monitor 的對象來完成,其實 Wait/Notify 等方法也依賴於 Monitor 對象。推薦閱讀:Synchronized 與 ReentrantLock 的區別!
這就是為什麼只有在同步的塊或者方法中才能調用 Wait/Notify 等方法,否則會拋出 java.lang.IllegalMonitorStateException 的異常。
我們再來看一下同步方法的反編譯結果,源程式碼如下:
package com.paddx.test.concurrent; public class SynchronizedMethod { public synchronized void method() { System.out.println("Hello World!"); } }
反編譯結果:

從反編譯的結果來看,方法的同步並沒有通過指令 Monitorenter 和 Monitorexit 來完成(理論上其實也可以通過這兩條指令來實現)。不過相對於普通方法,其常量池中多了 ACC_SYNCHRONIZED 標示符。
JVM 就是根據該標示符來實現方法的同步的:當方法調用時,調用指令將會檢查方法的 ACC_SYNCHRONIZED 訪問標誌是否被設置。
如果設置了,執行執行緒將先獲取 Monitor,獲取成功之後才能執行方法體,方法執行完後再釋放 Monitor。在方法執行期間,其他任何執行緒都無法再獲得同一個 Monitor 對象。
其實本質上沒有區別,只是方法的同步是一種隱式的方式來實現,無需通過位元組碼來完成。
運行結果解釋
有了對 Synchronized 原理的認識,再來看上面的程式就可以迎刃而解了。
①程式碼段 2 結果
雖然 Method1 和 Method2 是不同的方法,但是這兩個方法都進行了同步,並且是通過同一個對象去調用的。
所以調用之前都需要先去競爭同一個對象上的鎖(Monitor),也就只能互斥的獲取到鎖,因此,Method1 和 Method2 只能順序的執行。
②程式碼段 3 結果
雖然 Test 和 Test2 屬於不同對象,但是 Test 和 Test2 屬於同一個類的不同實例。
由於 Method1 和 Method2 都屬於靜態同步方法,所以調用的時候需要獲取同一個類上 Monitor(每個類只對應一個 Class 對象),所以也只能順序的執行。
③程式碼段 4 結果
對於程式碼塊的同步,實質上需要獲取 Synchronized 關鍵字後面括弧中對象的 Monitor。
由於這段程式碼中括弧的內容都是 This,而 Method1 和 Method2 又是通過同一的對象去調用的,所以進入同步塊之前需要去競爭同一個對象上的鎖,因此只能順序執行同步塊。
總結
Synchronized 是 Java 並發編程中最常用的用於保證執行緒安全的方式,其使用相對也比較簡單。
但是如果能夠深入了解其原理,對監視器鎖等底層知識有所了解,一方面可以幫助我們正確的使用 Synchronized 關鍵字。
另一方面也能夠幫助我們更好的理解並發編程機制,有助於我們在不同的情況下選擇更優的並發策略來完成任務。對平時遇到的各種並發問題,也能夠從容的應對。
原文鏈接:
https://www.cnblogs.com/paddix/p/5367116.html
Java一日一條
ID:mjx_java