搞定高并發(fā),豈能不懂Synchronized底層原理?
Synchronized 是 Java 中解決并發(fā)問(wèn)題的一種最常用的方法,也是最簡(jiǎn)單的一種方法。本文作者將全面剖析 Synchronized 的底層原理。
Synchronized 的基本使用
Synchronized 的作用主要有三個(gè):
- 確保線程互斥的訪問(wèn)同步代碼
- 保證共享變量的修改能夠及時(shí)可見(jiàn)
- 有效解決重排序問(wèn)題
從語(yǔ)法上講,Synchronized 總共有三種用法:
- 修飾普通方法
- 修飾靜態(tài)方法
- 修飾代碼塊
接下來(lái)我就通過(guò)幾個(gè)例子程序來(lái)說(shuō)明一下這三種使用方式(為了便于比較,三段代碼除了 Synchronized 的使用方式不同以外,其他基本保持一致)。
沒(méi)有同步的情況
代碼段 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();
- }
- }
執(zhí)行結(jié)果如下,線程 1 和線程 2 同時(shí)進(jìn)入執(zhí)行狀態(tài),線程 2 執(zhí)行速度比線程 1 快,所以線程 2 先執(zhí)行完成。
這個(gè)過(guò)程中線程 1 和線程 2 是同時(shí)執(zhí)行的:
- Method 1 start
- Method 1 execute
- Method 2 start
- Method 2 execute
- Method 2 end
- Method 1 end
對(duì)普通方法同步
代碼段 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();
- }
- }
執(zhí)行結(jié)果如下,跟代碼段 1 比較,可以很明顯的看出,線程 2 需要等待線程 1 的 Method1 執(zhí)行完成才能開(kāi)始執(zhí)行 Method2 方法。
- Method 1 start
- Method 1 execute
- Method 1 end
- Method 2 start
- Method 2 execute
- Method 2 end
靜態(tài)方法(類)同步
代碼段 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();
- }
- }
執(zhí)行結(jié)果如下,對(duì)靜態(tài)方法的同步本質(zhì)上是對(duì)類的同步(靜態(tài)方法本質(zhì)上是屬于類的方法,而不是對(duì)象上的方法)。
所以即使 Test 和 Test2 屬于不同的對(duì)象,但是它們都屬于 SynchronizedTest 類的實(shí)例。
所以也只能順序的執(zhí)行 Method1 和 Method2,不能并發(fā)執(zhí)行:
- 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();
- }
- }
執(zhí)行結(jié)果如下,雖然線程 1 和線程 2 都進(jìn)入了對(duì)應(yīng)的方法開(kāi)始執(zhí)行,但是線程 2 在進(jìn)入同步塊之前,需要等待線程 1 中同步塊執(zhí)行完成。
- Method 1 start
- Method 1 execute
- Method 2 start
- Method 1 end
- Method 2 execute
- Method 2 end
Synchronized 原理
如果對(duì)上面的執(zhí)行結(jié)果還有疑問(wèn),也先不用急,我們先來(lái)了解 Synchronized 的原理。
再回頭上面的問(wèn)題就一目了然了。我們先通過(guò)反編譯下面的代碼來(lái)看看 Synchronized 是如何實(shí)現(xiàn)對(duì)代碼塊進(jìn)行同步的:
- package com.paddx.test.concurrent;
- public class SynchronizedMethod {
- public synchronized void method() {
- System.out.println("Hello World!");
- }
- }
反編譯結(jié)果:
關(guān)于這兩條指令的作用,我們直接參考 JVM 規(guī)范中描述:
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.
這段話的大概意思為:每個(gè)對(duì)象有一個(gè)監(jiān)視器鎖(Monitor),當(dāng) Monitor 被占用時(shí)就會(huì)處于鎖定狀態(tài)。
線程執(zhí)行 Monitorenter 指令時(shí)嘗試獲取 Monitor 的所有權(quán),過(guò)程如下:
- 如果 Monitor 的進(jìn)入數(shù)為 0,則該線程進(jìn)入 Monitor,然后將進(jìn)入數(shù)設(shè)置為 1,該線程即為 Monitor 的所有者。
- 如果線程已經(jīng)占有該 Monitor,只是重新進(jìn)入,則進(jìn)入 Monitor 的進(jìn)入數(shù)加 1。
- 如果其他線程已經(jīng)占用了 Monitor,則該線程進(jìn)入阻塞狀態(tài),直到 Monitor 的進(jìn)入數(shù)為 0,再重新嘗試獲取 Monitor 的所有權(quán)。
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.
這段話的大概意思為:執(zhí)行 Monitorexit 的線程必須是 Objectref 所對(duì)應(yīng)的 Monitor 的所有者。
指令執(zhí)行時(shí),Monitor 的進(jìn)入數(shù)減 1,如果減 1 后進(jìn)入數(shù)為 0,那線程退出 Monitor,不再是這個(gè) Monitor 的所有者。
其他被這個(gè) Monitor 阻塞的線程可以嘗試去獲取這個(gè) Monitor 的所有權(quán)。
通過(guò)這兩段描述,我們應(yīng)該能很清楚的看出 Synchronized 的實(shí)現(xiàn)原理。
Synchronized 的語(yǔ)義底層是通過(guò)一個(gè) Monitor 的對(duì)象來(lái)完成,其實(shí) Wait/Notify 等方法也依賴于 Monitor 對(duì)象。
這就是為什么只有在同步的塊或者方法中才能調(diào)用 Wait/Notify 等方法,否則會(huì)拋出 java.lang.IllegalMonitorStateException 的異常。
我們?cè)賮?lái)看一下同步方法的反編譯結(jié)果,源代碼如下:
- package com.paddx.test.concurrent;
- public class SynchronizedMethod {
- public synchronized void method() {
- System.out.println("Hello World!");
- }
- }
反編譯結(jié)果:
從反編譯的結(jié)果來(lái)看,方法的同步并沒(méi)有通過(guò)指令 Monitorenter 和 Monitorexit 來(lái)完成(理論上其實(shí)也可以通過(guò)這兩條指令來(lái)實(shí)現(xiàn))。不過(guò)相對(duì)于普通方法,其常量池中多了 ACC_SYNCHRONIZED 標(biāo)示符。
JVM 就是根據(jù)該標(biāo)示符來(lái)實(shí)現(xiàn)方法的同步的:當(dāng)方法調(diào)用時(shí),調(diào)用指令將會(huì)檢查方法的 ACC_SYNCHRONIZED 訪問(wèn)標(biāo)志是否被設(shè)置。
如果設(shè)置了,執(zhí)行線程將先獲取 Monitor,獲取成功之后才能執(zhí)行方法體,方法執(zhí)行完后再釋放 Monitor。在方法執(zhí)行期間,其他任何線程都無(wú)法再獲得同一個(gè) Monitor 對(duì)象。
其實(shí)本質(zhì)上沒(méi)有區(qū)別,只是方法的同步是一種隱式的方式來(lái)實(shí)現(xiàn),無(wú)需通過(guò)字節(jié)碼來(lái)完成。
運(yùn)行結(jié)果解釋
有了對(duì) Synchronized 原理的認(rèn)識(shí),再來(lái)看上面的程序就可以迎刃而解了。
①代碼段 2 結(jié)果
雖然 Method1 和 Method2 是不同的方法,但是這兩個(gè)方法都進(jìn)行了同步,并且是通過(guò)同一個(gè)對(duì)象去調(diào)用的。
所以調(diào)用之前都需要先去競(jìng)爭(zhēng)同一個(gè)對(duì)象上的鎖(Monitor),也就只能互斥的獲取到鎖,因此,Method1 和 Method2 只能順序的執(zhí)行。
②代碼段 3 結(jié)果
雖然 Test 和 Test2 屬于不同對(duì)象,但是 Test 和 Test2 屬于同一個(gè)類的不同實(shí)例。
由于 Method1 和 Method2 都屬于靜態(tài)同步方法,所以調(diào)用的時(shí)候需要獲取同一個(gè)類上 Monitor(每個(gè)類只對(duì)應(yīng)一個(gè) Class 對(duì)象),所以也只能順序的執(zhí)行。
③代碼段 4 結(jié)果
對(duì)于代碼塊的同步,實(shí)質(zhì)上需要獲取 Synchronized 關(guān)鍵字后面括號(hào)中對(duì)象的 Monitor。
由于這段代碼中括號(hào)的內(nèi)容都是 This,而 Method1 和 Method2 又是通過(guò)同一的對(duì)象去調(diào)用的,所以進(jìn)入同步塊之前需要去競(jìng)爭(zhēng)同一個(gè)對(duì)象上的鎖,因此只能順序執(zhí)行同步塊。
總結(jié)
Synchronized 是 Java 并發(fā)編程中最常用的用于保證線程安全的方式,其使用相對(duì)也比較簡(jiǎn)單。
但是如果能夠深入了解其原理,對(duì)監(jiān)視器鎖等底層知識(shí)有所了解,一方面可以幫助我們正確的使用 Synchronized 關(guān)鍵字。
另一方面也能夠幫助我們更好的理解并發(fā)編程機(jī)制,有助于我們?cè)诓煌那闆r下選擇更優(yōu)的并發(fā)策略來(lái)完成任務(wù)。對(duì)平時(shí)遇到的各種并發(fā)問(wèn)題,也能夠從容的應(yīng)對(duì)。