Java併發編程:synchronized和鎖優化

来源:https://www.cnblogs.com/butterfly100/archive/2018/04/11/8786856.html
-Advertisement-
Play Games

1. 使用方法 synchronized 是 java 中最常用的保證線程安全的方式,synchronized 的作用主要有三方面: 語義上來講,synchronized主要有三種用法: 2. 實現原理 2.1. 監視器鎖 synchronized 同步代碼塊的語義底層是基於對象內部的監視器鎖(mo ...


1. 使用方法

synchronized 是 java 中最常用的保證線程安全的方式,synchronized 的作用主要有三方面:

  1. 確保線程互斥的訪問代碼塊,同一時刻只有一個方法可以進入到臨界區
  2. 保證共用變數的修改能及時可見
  3. 有效解決重排序問題

語義上來講,synchronized主要有三種用法:

  1. 修飾普通方法,鎖的是當前對象實例(this)
  2. 修飾靜態方法,鎖的是當前 Class 對象(靜態方法是屬於類,而不是對象)
  3. 修飾代碼塊,鎖的是括弧里的對象

 

2. 實現原理

2.1. 監視器鎖

synchronized 同步代碼塊的語義底層是基於對象內部的監視器鎖(monitor),分別是使用 monitorenter 和 monitorexit 指令完成。其實 wait/notify 也依賴於 monitor 對象,所以其一般要在 synchronized 同步的方法或代碼塊內使用。monitorenter 指令在編譯為位元組碼後插入到同步代碼塊的開始位置,monitorexit 指令在編譯為位元組碼後插入到方法結束處和異常處。JVM 要保證每個 monitorenter 必須有對應的 moniorexit。

monitorenter:每個對象都有一個監視器鎖(monitor),當 monitor 被某個線程占用時就會處於鎖定狀態,線程執行 monitorenter 指令時嘗試獲得 monitor 的所有權,即嘗試獲取對象的鎖。過程如下:

  1. 如果 monitor 的進入數為0,則該線程進入 monitor,然後將進入數設置為1,該線程即為 monitor 的所有者;
  2. 如果線程已經占有monitor,只是重新進入,則monitor的進入數+1;
  3. 如果其他線程已經占用 monitor,則該線程處於阻塞狀態,直至 monitor 的進入數為0,再重新嘗試獲得 monitor 的所有權

monitorexit:執行 monitorexit 的線程必須是 objectref 所對應的 monitor 的所有者。執行指令時,monitor 的進入數減1,如果減1後進入數為0,則線程退出 monitor,不再是這個 monitor 的所有者,其他被這個 monitor 阻塞的線程可以嘗試獲取這個 monitor 的所有權。

 

2.2. 線程狀態和狀態轉化

在 HotSpot JVM 中,monitor 由 ObjectMonitor 實現,其主要數據結構如下:

ObjectMonitor() {
    _header       = NULL;
    _count        = 0;      //記錄個數
    _waiters      = 0,
    _recursions   = 0;
    _object       = NULL;
    _owner        = NULL;   //持有monitor的線程
    _WaitSet      = NULL;   //處於wait狀態的線程,會被加入到_WaitSet
    _WaitSetLock  = 0 ;
    _Responsible  = NULL ;
    _succ         = NULL ;
    _cxq          = NULL ;
    FreeNext      = NULL ;
    _EntryList    = NULL ;  //處於等待鎖block狀態的線程,會被加入到該列表
    _SpinFreq     = 0 ;
    _SpinClock    = 0 ;
    OwnerIsThread = 0 ;
  }

ObjectMonitor 中有兩個隊列,_WaitSet 和 _EntryList,用來保存 ObjectWaiter 對象列表(每個等待鎖的線程都會被封裝成 ObjectWaiter 對象),_owner 指向持有 ObjectMonitor 對象的線程。

  1. 當多個線程同時訪問一段同步代碼時,首先會進入 _EntryList,等待鎖處於阻塞狀態。
  2. 當線程獲取到對象的 monitor 後進入 The Owner 區域,並把 ObjectMonitor 中的 _owner 變數設置為當前線程,同時 monitor 中的計數器 count 加1。
  3. 若線程調用 wait() 方法,將釋放當前持有的 monitor,_owner 變數恢復為 null,count 減1,同時該線程進入 _WaitSet 集合中等待被喚醒,處於 waiting 狀態。
  4. 若當前線程執行完畢,將釋放 monitor 並複位變數的值,以便其他線程進入獲取 monitor。

過程如下圖所示: 

 

 

3. 鎖優化

在 JDK1.6 之後,出現了各種鎖優化技術,如輕量級鎖、偏向鎖、適應性自旋、鎖粗化、鎖消除等,這些技術都是為了線上程間更高效的解決競爭問題,從而提升程式的執行效率。

通過引入輕量級鎖和偏向鎖來減少重量級鎖的使用。鎖的狀態總共分四種:無鎖狀態、偏向鎖、輕量級鎖和重量級鎖。鎖隨著競爭情況可以升級,但鎖升級後不能降級,意味著不能從輕量級鎖狀態降級為偏向鎖狀態,也不能從重量級鎖狀態降級為輕量級鎖狀態。

無鎖狀態 → 偏向鎖狀態 → 輕量級鎖 → 重量級鎖

3.1. 對象頭

要理解輕量級鎖和偏向鎖的運行機制,還要從瞭解對象頭(Object Header)開始。對象頭分為兩部分:

1、Mark Word:存儲對象自身的運行時數據,如:Hash Code,GC 分代年齡、鎖信息。這部分數據在32位和64位的 JVM 中分別為 32bit 和 64bit。考慮空間效率,Mark Word 被設計為非固定的數據結構,以便在極小的空間記憶體儲儘量多的信息,32bit的 Mark Word 如下圖所示: 

2、存儲指向方法區對象類型數據的指針,如果是數組對象的話,額外會存儲數組的長度

 

3.2. 重量級鎖

monitor 監視器鎖本質上是依賴操作系統的 Mutex Lock 互斥量 來實現的,我們一般稱之為重量級鎖。因為 OS 實現線程間的切換需要從用戶態轉換到核心態,這個轉換過程成本較高,耗時相對較長,因此 synchronized 效率會比較低。

重量級鎖的鎖標誌位為'10',指針指向的是 monitor 對象的起始地址,關於 monitor 的實現原理上文已經描述了。

 

3.3. 輕量級鎖

輕量級鎖是相對基於OS的互斥量實現的重量級鎖而言的,它的本意是在沒有多線程競爭的前提下,減少傳統的重量級鎖使用OS的互斥量而帶來的性能消耗。

輕量級鎖提升性能的經驗依據是:對於絕大部分鎖,在整個同步周期內都是不存在競爭的。如果沒有競爭,輕量級鎖就可以使用 CAS 操作避免互斥量的開銷,從而提升效率。

輕量級鎖的加鎖過程: 

1、線程在進入到同步代碼塊的時候,JVM 會先在當前線程的棧幀中建立一個名為鎖記錄(Lock Record)的空間,用於存儲鎖對象當前 Mark Word 的拷貝(官方稱為 Displaced Mark Word),owner 指針指向對象的 Mark Word。此時堆棧與對象頭的狀態如圖所示: 

2、JVM 使用 CAS 操作嘗試將對象頭中的 Mark Word 更新為指向 Lock Record 的指針。如果更新成功,則執行步驟3;更新失敗,則執行步驟4

3、如果更新成功,那麼這個線程就擁有了該對象的鎖,對象的 Mark Word 的鎖狀態為輕量級鎖(標誌位轉變為'00')。此時線程堆棧與對象頭的狀態如圖所示: 

4、如果更新失敗,JVM 首先檢查對象的 Mark Word 是否指向當前線程的棧幀

  • 如果是,就說明當前線程已經擁有了該對象的鎖,那就可以直接進入同步代碼塊繼續執行
  • 如果不是,就說明這個鎖對象已經被其他的線程搶占了,當前線程會嘗試自旋一定次數來獲取鎖。如果自旋一定次數 CAS 操作仍沒有成功,那麼輕量級鎖就要升級為重量級鎖(鎖的標誌位轉變為'10'),Mark Word 中存儲的就是指向重量級鎖的指針,後面等待鎖的線程也就進入阻塞狀態

輕量級鎖的解鎖過程: 

1、通過 CAS 操作用線程中複製的 Displaced Mark Word 中的數據替換對象當前的 Mark Word 

2、如果替換成功,整個同步過程就完成了 

3、如果替換失敗,說明有其他線程嘗試過獲取該鎖,那就在釋放鎖的同時,喚醒被掛起的線程

 

3.4. 偏向鎖

輕量級鎖是在無多線程競爭的情況下,使用 CAS 操作去消除互斥量;偏向鎖是在無多線程競爭的情況下,將這個同步都消除掉。

偏向鎖提升性能的經驗依據是:對於絕大部分鎖,在整個同步周期內不僅不存在競爭,而且總由同一線程多次獲得。偏向鎖會偏向第一個獲得它的線程,如果接下來的執行過程中,該鎖沒有被其他線程獲取,則持有偏向鎖的線程不需要再進行同步。這使得線程獲取鎖的代價更低。

偏向鎖的獲取過程: 

1、線程執行同步塊,鎖對象第一次被獲取的時候,JVM 會將鎖對象的 Mark Word 中的鎖狀態設置為偏向鎖(鎖標誌位為'01',是否偏向的標誌位為'1'),同時通過 CAS 操作在 Mark Word 中記錄獲取到這個鎖的線程的 ThreadID

2、如果 CAS 操作成功。持有偏向鎖的線程每次進入和退出同步塊時,只需測試一下 Mark Word 里是否存儲著當前線程的 ThreadID。如果是,則表示線程已經獲得了鎖,而不需要額外花費 CAS 操作加鎖和解鎖

3、如果不是,則通過CAS操作競爭鎖,競爭成功,則將 Mark Word 的 ThreadID 替換為當前線程的 ThreadID

偏向鎖的釋放過程: 

1、當一個線程已經持有偏向鎖,而另外一個線程嘗試競爭偏向鎖時,CAS 替換 ThreadID 操作失敗,則開始撤銷偏向鎖。偏向鎖的撤銷,需要等待原持有偏向鎖的線程到達全局安全點(在這個時間點上沒有位元組碼正在執行),暫停該線程,並檢查其狀態

2、如果原持有偏向鎖的線程不處於活動狀態或已退出同步代碼塊,則該線程釋放鎖。將對象頭設置為無鎖狀態(鎖標誌位為'01',是否偏向標誌位為'0')

3、如果原持有偏向鎖的線程未退出同步代碼塊,則升級為輕量級鎖(鎖標誌位為'00')

 

3.5. 總結

偏向鎖、輕量級鎖、重量級鎖之間的狀態轉換如圖所示(概括上文描述的鎖獲取和釋放的內容): 

下麵是這幾種鎖的比較:

 

3.6. 其他優化

1、適應性自旋 

自旋鎖:互斥同步時,掛起和恢複線程都需要切換到內核態完成,這對性能併發帶來了不少的壓力。同時在許多應用上,共用數據的鎖定狀態只會持續很短的一段時間,為了這段較短的時間而去掛起和恢複線程並不值得。那麼如果有多個線程同時並行執行,可以讓後面請求鎖的線程通過自旋(CPU忙迴圈執行空指令)的方式稍等一會兒,看看持有鎖的線程是否會很快的釋放鎖,這樣就不需要放棄 CPU 的執行時間了

適應性自旋:在輕量級鎖獲取過程中,線程執行 CAS 操作失敗時,需要通過自旋來獲取重量級鎖。如果鎖被占用的時間比較短,那麼自旋等待的效果就會比較好,而如果鎖占用的時間很長,自旋的線程則會白白浪費 CPU 資源。解決這個問題的最簡答的辦法就是:指定自旋的次數,如果在限定次數內還沒獲取到鎖(例如10次),就按傳統的方式掛起線程進入阻塞狀態。JDK1.6 之後引入了自適應性自旋的方式,如果在同一鎖對象上,一線程自旋等待剛剛成功獲得鎖,並且持有鎖的線程正在運行中,那麼 JVM 會認為這次自旋也有可能再次成功獲得鎖,進而允許自旋等待相對更長的時間(例如100次)。另一方面,如果某個鎖自旋很少成功獲得,那麼以後要獲得這個鎖時將省略自旋過程,以避免浪費 CPU。

2、鎖消除 

鎖消除就是編譯器運行時,對一些被檢測到不可能存在共用數據競爭的鎖進行消除。如果判斷一段代碼中,堆上的數據不會逃逸出去從而被其他線程訪問到,則可以把他們當做棧上的數據對待,認為它們是線程私有的,不必要加鎖。

public String concatString(String s1, String s2, String s3) {
    StringBuffer sb = new StringBuffer();
    sb.append("a");
    sb.append("b");
    sb.append("c");
    return sb.toString();
}

 

在 StringBuffer.append() 方法中有一個同步代碼塊,鎖就是sb對象,但 sb 的所有引用不會逃逸到 concatString() 方法外部,其他線程無法訪問它。因此這裡有鎖,但是在即時編譯之後,會被安全的消除掉,忽略掉同步而直接執行了。

3、鎖粗化 

鎖粗化就是 JVM 檢測到一串零碎的操作都對同一個對象加鎖,則會把加鎖同步的範圍粗化到整個操作序列的外部。以上述 concatString() 方法為例,內部的 StringBuffer.append() 每次都會加鎖,將會鎖粗化,在第一次 append() 前至 最後一個 append() 後只需要加一次鎖就可以了。

4. 參考

《深入理解Java虛擬機》- 周志明 
Java Synchronised機制 
Java synchronized 關鍵字的實現原理

---

我的博客即將搬運同步至騰訊雲+社區,邀請大家一同入駐:https://cloud.tencent.com/developer/support-plan?invite_code=12mihsfip6v9b


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

-Advertisement-
Play Games
更多相關文章
  • 本文介紹的 Chrome 開發者工具基於 Chrome 65版本,如果你的 Chrome 開發者工具沒有下文提到的那些內容,請檢查下 Chrome 的版本 簡介 Chrome 開發者工具是一套內置於 Google Chrome 中的Web開發和調試工具,可用來對網站進行迭代、調試和分析 打開 Chr ...
  • 本文最初發表於 "博客園" ,併在 "GitHub" 上持續更新 前端的系列文章 。歡迎在GitHub上關註我,一起入門和進階前端。 以下是正文。 我前幾天寫過一篇文章: "《裸辭兩個月,海投一個月,從Android轉戰Web前端的求職之路》" 。這篇文章講述了我在轉型過程中的親身經歷和感受,不少童 ...
  • 藉助百度地圖的 LocalSearch 和 Autocomplete 兩個方法 實現方式:通過promise以及百度地圖的callback回調函數 map.js 1 export function MP(ak) { 2 return new Promise(function (resolve, re ...
  • 為了保證應用的高可用和高併發性,一般都會部署多個節點;對於定時任務,如果每個節點都執行自己的定時任務,一方面耗費了系統資源,另一方面有些任務多次執行,可能引發應用邏輯問題,所以需要一個分散式的調度系統,來協調每個節點執行定時任務。 ...
  • 一、場景描述 代理在生活中並不少見,租房子需要找中介,打官司需要找律師,很多事情我們需要找專業人士代理我們做,另一方面,中介和律師也代理了房東、法律程式與我們打交道。 當然,設計模式中的代理與廣義的代理還是有所差別的;A對象調用B對象提供的服務X時,使用代理模式的前提是B對象實現了IB介面,通過介面 ...
  • 前不久需要開闢一個防火牆,所以要畫一個系統圖,其中有個箭頭連線需要設置一下,其它都還簡單算是(托拉拽)。用的是微軟的office-Visio工具: 點home的connector,前提已經選好一條畫好的連接線,然後選擇line下的Arrows的箭頭樣式。 ...
  • Struts1,一個中心控制器,XML定製轉向URL,Action處理邏輯。 Struts2,工作流程,用攔截器攔截用戶請求,把用戶的業務邏輯控制器和Servlet分離,生成控制器代理回調業務控制器的execute方法處理用戶請求,再通過處理用戶請求後的返回值進行用戶視圖呈現。 (拋棄struts1 ...
  • I am having some trouble getting the JSONCPP Library into Visual Studio. I have downloaded the library I am just unsure how to import it into my proje ...
一周排行
    -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.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...