天天用Synchronized,底層原理是個啥?

来源:https://www.cnblogs.com/javastack/archive/2020/05/07/12844891.html
-Advertisement-
Play Games

作者:iuxiaopeng https://www.cnblogs.com/paddix/p/5367116.html Synchronized 的基本使用 Synchronized 的作用主要有三個: 確保線程互斥的訪問同步代碼 保證共用變數的修改能夠及時可見 有效解決重排序問題 從語法上講,Sy ...


作者:iuxiaopeng
https://www.cnblogs.com/paddix/p/5367116.html

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 關鍵字。

另一方面也能夠幫助我們更好的理解併發編程機制,有助於我們在不同的情況下選擇更優的併發策略來完成任務。對平時遇到的各種併發問題,也能夠從容的應對。

推薦去我的博客閱讀更多:

1.Java JVM、集合、多線程、新特性系列教程

2.Spring MVC、Spring Boot、Spring Cloud 系列教程

3.Maven、Git、Eclipse、Intellij IDEA 系列工具教程

4.Java、後端、架構、阿裡巴巴等大廠最新面試題

覺得不錯,別忘了點贊+轉發哦!


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 案例故事: Testlink是我們常用的用例管理工具,很多公司其實都在用, Testlink 支持制定測試計劃,支持多人線上同時管理維護/執行測試用例,自動生成測試報告等。 我個人也非常非常不推薦Excel線下管理測試用例, 但是官方提供的Testlink版本,是不支持Excel導入的,只能進行Xm ...
  • flutter sdk安裝包 【下載地址】https://flutter.dev/docs/development/tools/sdk/releases#macos 下載完成後,操作步驟: cd ~/目標路徑 unzip ~/安裝包路徑 設置&更新環境變數 打開 $HOME/.bash_profil ...
  • 前言 本文的文字及圖片來源於網路,僅供學習、交流使用,不具有任何商業用途,版權歸原作者所有,如有問題請及時聯繫我們以作處理。 作者:William Mannard 歡迎點擊右上角關註小編,除了分享技術文章之外還有很多福利,私信學習資料可以領取包括不限於Python實戰演練、PDF電子文檔、面試集錦、 ...
  • 1. 簡介 在JDK中 適用於載入資源的類,但是 的實現類都是訪問網路資源的,並沒有可以從類路徑或者相對路徑獲取文件及 , 雖然可以通過自定義擴展URL介面來實現新的處理程式,但是這是非常複雜的,同時 介面中定義的行為也並不是很理想的 ,如檢測資源的存在等的行為,這也是 為什麼自己全新的開發一套自己 ...
  • 一、介面的預設方法 Java 8允許我們給介面添加一個非抽象的方法實現,只需要使用 default關鍵字即可,這個特征又叫做擴展方法,示例如下: interface Formula { double calculate(int a); default double sqrt(int a) { ret ...
  • 1.eclipse的版本介紹 所有版本的eclipse都按照系統分為32位和64位,如果你的JDK是32位,eclispe必須也是32位,eclipse應與JDK相符合 eclipse分SE和EE等版本,基礎篇SE版本即可,EE版本可向下相容SE版本,SE版本不能向上相容EE版本 2.下載和安裝ec ...
  • 項目簡介 項目來源於: "https://gitee.com/huang_xiao_feng/carrentalsystem" 本系統基於 Maven+JSP+SSM+Mysql 實現的汽車租賃管理系統。簡單實現了基礎管理、系統管理,業務管理和統計分析。 難度等級:中等 技術棧 編輯器 Intell ...
  • python在通過cmd安裝pandas時遇到ModuleNotFoundError: No module named 'pip'的報錯。 網上查詢後通過如下兩行cmd命令解決了 然後就可通過cmd安裝pandas啦 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...