死磕 java同步系列之synchronized解析

来源:https://www.cnblogs.com/tong-yuan/archive/2019/05/21/synchronized.html
-Advertisement-
Play Games

synchronized的特性? synchronized的實現原理? synchronized是否可重入? synchronized是否是公平鎖? synchronized的優化? synchronized的五種使用方式? ...


問題

(1)synchronized的特性?

(2)synchronized的實現原理?

(3)synchronized是否可重入?

(4)synchronized是否是公平鎖?

(5)synchronized的優化?

(6)synchronized的五種使用方式?

簡介

synchronized關鍵字是Java裡面最基本的同步手段,它經過編譯之後,會在同步塊的前後分別生成 monitorenter 和 monitorexit 位元組碼指令,這兩個位元組碼指令都需要一個引用類型的參數來指明要鎖定和解鎖的對象。

實現原理

在學習Java記憶體模型的時候,我們介紹過兩個指令:lock 和 unlock。

lock,鎖定,作用於主記憶體的變數,它把主記憶體中的變數標識為一條線程獨占狀態。

unlock,解鎖,作用於主記憶體的變數,它把鎖定的變數釋放出來,釋放出來的變數才可以被其它線程鎖定。

但是這兩個指令並沒有直接提供給用戶使用,而是提供了兩個更高層次的指令 monitorenter 和 monitorexit 來隱式地使用 lock 和 unlock 指令。

而 synchronized 就是使用 monitorenter 和 monitorexit 這兩個指令來實現的。

根據JVM規範的要求,在執行monitorenter指令的時候,首先要去嘗試獲取對象的鎖,如果這個對象沒有被鎖定,或者當前線程已經擁有了這個對象的鎖,就把鎖的計數器加1,相應地,在執行monitorexit的時候會把計數器減1,當計數器減小為0時,鎖就釋放了。

我們還是來上一段代碼,看看編譯後的位元組碼長啥樣來學習:

public class SynchronizedTest {

    public static void sync() {
        synchronized (SynchronizedTest.class) {
            synchronized (SynchronizedTest.class) {
            }
        }
    }

    public static void main(String[] args) {

    }
}

我們這段代碼很簡單,只是簡單地對SynchronizedTest.class對象加了兩次synchronized,除此之外,啥也沒乾。

編譯後的sync()方法的位元組碼指令如下,為了便於閱讀,彤哥特意加上了註釋:

// 載入常量池中的SynchronizedTest類對象到操作數棧中
0 ldc #2 <com/coolcoding/code/synchronize/SynchronizedTest>
// 複製棧頂元素
2 dup
// 存儲一個引用到本地變數0中,後面的0表示第幾個變數
3 astore_0
// 調用monitorenter,它的參數變數0,也就是上面的SynchronizedTest類對象
4 monitorenter
// 再次載入常量池中的SynchronizedTest類對象到操作數棧中
5 ldc #2 <com/coolcoding/code/synchronize/SynchronizedTest>
// 複製棧頂元素
7 dup
// 存儲一個引用到本地變數1中
8 astore_1
// 再次調用monitorenter,它的參數是變數1,也還是SynchronizedTest類對象
9 monitorenter
// 從本地變數表中載入第1個變數
10 aload_1
// 調用monitorexit解鎖,它的參數是上面載入的變數1
11 monitorexit
// 跳到第20行
12 goto 20 (+8)
15 astore_2
16 aload_1
17 monitorexit
18 aload_2
19 athrow
// 從本地變數表中載入第0個變數
20 aload_0
// 調用monitorexit解鎖,它的參數是上面載入的變數0
21 monitorexit
// 跳到第30行
22 goto 30 (+8)
25 astore_3
26 aload_0
27 monitorexit
28 aload_3
29 athrow
// 方法返回,結束
30 return

按照彤哥的註釋讀起來,位元組碼比較簡單,我們的synchronized鎖定的是SynchronizedTest類對象,可以看到它從常量池中載入了兩次SynchronizedTest類對象,分別存儲在本地變數0和本地變數1中,解鎖的時候正好是相反的順序,先解鎖變數1,再解鎖變數0,實際上變數0和變數1指向的是同一個對象,所以synchronized是可重入的。

至於,被加鎖的對象具體在對象頭中是怎麼存儲的,彤哥這裡就不細講了,有興趣的可以看看《Java併發編程的藝術》這本書。

公眾號後臺回覆“JMM”可領取這本書籍的pdf版。

原子性、可見性、有序性

前面講解Java記憶體模型的時候我們說過記憶體模型主要就是用來解決緩存一致性的問題的,而緩存一致性主要包括原子性、可見性、有序性。

那麼,synchronized關鍵字能否保證這三個特性呢?

還是回到Java記憶體模型上來,synchronized關鍵字底層是通過monitorenter和monitorexit實現的,而這兩個指令又是通過lock和unlock來實現的。

而lock和unlock在Java記憶體模型中是必須滿足下麵四條規則的:

(1)一個變數同一時刻只允許一條線程對其進行lock操作,但lock操作可以被同一個線程執行多次,多次執行lock後,只有執行相同次數的unlock操作,變數才能被解鎖。

(2)如果對一個變數執行lock操作,將會清空工作記憶體中此變數的值,在執行引擎使用這個變數前,需要重新執行load或assign操作初始化變數的值;

(3)如果一個變數沒有被lock操作鎖定,則不允許對其執行unlock操作,也不允許unlock一個其它線程鎖定的變數;

(4)對一個變數執行unlock操作之前,必須先把此變數同步回主記憶體中,即執行store和write操作;

通過規則(1),我們知道對於lock和unlock之間的代碼,同一時刻只允許一個線程訪問,所以,synchronized是具有原子性的。

通過規則(1)(2)和(4),我們知道每次lock和unlock時都會從主記憶體載入變數或把變數刷新回主記憶體,而lock和unlock之間的變數(這裡是指鎖定的變數)是不會被其它線程修改的,所以,synchronized是具有可見性的。

通過規則(1)和(3),我們知道所有對變數的加鎖都要排隊進行,且其它線程不允許解鎖當前線程鎖定的對象,所以,synchronized是具有有序性的。

綜上所述,synchronized是可以保證原子性、可見性和有序性的。

公平鎖 VS 非公平鎖

通過上面的學習,我們知道了synchronized的實現原理,並且它是可重入的,那麼,它是否是公平鎖呢?

直接上菜:

public class SynchronizedTest {

    public static void sync(String tips) {
        synchronized (SynchronizedTest.class) {
            System.out.println(tips);
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        new Thread(()->sync("線程1")).start();
        Thread.sleep(100);
        new Thread(()->sync("線程2")).start();
        Thread.sleep(100);
        new Thread(()->sync("線程3")).start();
        Thread.sleep(100);
        new Thread(()->sync("線程4")).start();
    }
}

在這段程式中,我們起了四個線程,且分別間隔100ms啟動,每個線程裡面列印一句話後等待1000ms,如果synchronized是公平鎖,那麼列印的結果應該依次是 線程1、2、3、4。

但是,實際運行的結果幾乎不會出現上面的樣子,所以,synchronized是一個非公平鎖。

鎖優化

Java在不斷進化,同樣地,Java中像synchronized這種古老的東西也在不斷進化,比如ConcurrentHashMap在jdk7的時候還是使用ReentrantLock加鎖的,在jdk8的時候已經換成了原生的synchronized了,可見synchronized有原生的支持,它的進化空間還是很大的。

那麼,synchronized有哪些進化中的狀態呢?

我們這裡稍做一些簡單地介紹:

(1)偏向鎖,是指一段同步代碼一直被一個線程訪問,那麼這個線程會自動獲取鎖,降低獲取鎖的代價。

(2)輕量級鎖,是指當鎖是偏向鎖時,被另一個線程所訪問,偏向鎖會升級為輕量級鎖,這個線程會通過自旋的方式嘗試獲取鎖,不會阻塞,提高性能。

(3)重量級鎖,是指當鎖是輕量級鎖時,當自旋的線程自旋了一定的次數後,還沒有獲取到鎖,就會進入阻塞狀態,該鎖升級為重量級鎖,重量級鎖會使其他線程阻塞,性能降低。

總結

(1)synchronized在編譯時會在同步塊前後生成monitorenter和monitorexit位元組碼指令;

(2)monitorenter和monitorexit位元組碼指令需要一個引用類型的參數,基本類型不可以哦;

(3)monitorenter和monitorexit位元組碼指令更底層是使用Java記憶體模型的lock和unlock指令;

(4)synchronized是可重入鎖;

(5)synchronized是非公平鎖;

(6)synchronized可以同時保證原子性、可見性、有序性;

(7)synchronized有三種狀態:偏向鎖、輕量級鎖、重量級鎖;

彩蛋——synchronized的五種使用方式

通過上面的分析,我們知道synchronized是需要一個引用類型的參數的,而這個引用類型的參數在Java中其實可以分成三大類:類對象、實例對象、普通引用,使用方式分別如下:

public class SynchronizedTest2 {

    public static final Object lock = new Object();

    // 鎖的是SynchronizedTest.class對象
    public static synchronized void sync1() {

    }

    public static void sync2() {
        // 鎖的是SynchronizedTest.class對象
        synchronized (SynchronizedTest.class) {

        }
    }

    // 鎖的是當前實例this
    public synchronized void sync3() {

    }

    public void sync4() {
        // 鎖的是當前實例this
        synchronized (this) {

        }
    }

    public void sync5() {
        // 鎖的是指定對象lock
        synchronized (lock) {

        }
    }
}

在方法上使用synchronized的時候要註意,會隱式傳參,分為靜態方法和非靜態方法,靜態方法上的隱式參數為當前類對象,非靜態方法上的隱式參數為當前實例this。

另外,多個synchronized只有鎖的是同一個對象,它們之間的代碼才是同步的,這一點在使用synchronized的時候一定要註意。

推薦閱讀

  1. 死磕 java同步系列之JMM(Java Memory Model)

  2. 死磕 java同步系列之volatile解析


歡迎關註我的公眾號“彤哥讀源碼”,查看更多源碼系列文章, 與彤哥一起暢游源碼的海洋。

qrcode


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

-Advertisement-
Play Games
更多相關文章
  • 1.代碼生成器: [正反雙向](單表、主表、明細表、樹形表,快速開發利器)freemaker模版技術 ,0個代碼不用寫,生成完整的一個模塊,帶頁面、建表sql腳本、處理類、service等完整模塊2.多數據源:(支持同時連接無數個資料庫,可以不同的模塊連接不同數的據庫)支持N個數據源3.阿裡資料庫連 ...
  • 博主剛剛接觸web開發的時候,寫了一個介面 /get_article_info/1 獲取id為1的這篇文章的內容,被前輩們看見了,前輩給我說我這個介面設計的不太好啊,不符合RESTFUL規範,當前輩們說出這些話的時候,我很迷惑,我寫的介面不能夠好好工作嗎?能夠正常返回內容啊,對於不存在的文章也能夠在 ...
  • 這篇主要介紹 相關內容,主要是一些基本概念普及。 代理模式 1、什麼是代理模式? ,為其他對象提供一種代理以控制對這個對象的訪問。[DP] 通俗的說就是指客戶端並不直接調用實際的對象,而是通過代理對象,來間接調用實際的對象。 2、實現原理 可以是介面,也可以是抽象類 內部含有對真實對象RealSub ...
  • [TOC] 前言 很久沒有寫博客了,一直給自己找藉口說太忙了,過幾天有空再寫,幾天之後又幾天,時間就這麼快速的消逝。說到底就是自己太懶了,不下點決心真是不行。我決定逼自己一把,從今天開始學習設計模式系列,並寫成博文記錄下來,做不到的話,就罰自己一個月不玩游戲 (作孽啊。。。。) 六大原則 言歸正傳, ...
  • 我們在這篇通過一個具體CQRS-Reader-Actor的例子來示範akka-persistence的query端編程和應用。在前面的博客里我們設計了一個CQRS模式POS機程式的操作動作錄入過程,並示範瞭如何實現CQRS的寫端編程。現在我們可以根據這個例子來示範如何通過CQRS的讀端reader- ...
  • 人生苦短 我選python 全民學python的熱潮已經開啟,然而,對於這種情況,還是有很多小伙伴私信我python到底該怎麼入門?沒接觸過編程能學會嗎? 現在網上學習資料一搜一大把,正因為資料多了導致我們不知道如何是好! 一個朋友問我:有個朋友要學習 python,她屬於那種特別能啃書的,讓我推薦 ...
  • 第1題: Python如何爬取 HTTPS 網站? 這類問題屬於簡單類問題 在使用 requests 前加入:requests.packages.urllib3.disable_warnings()。 為 requests 添加 verify=False 參數 導入ssl模塊 第2題: 函數參數傳遞 ...
  • 1 Object類的特點2 日期類:如何輸出當前的日期 日期的相關方法3 如何用System類獲取當前的毫秒值 1 Object toString方法 equals(); ***​ 1.1 概述 java.lang.Object java語言中的根類 所有類的父類 ​ 假如一個類沒有特別指定的父類 ...
一周排行
    -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.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...