Java多線程與併發模型之鎖

来源:http://www.cnblogs.com/rese-t/archive/2017/11/17/7853270.html
-Advertisement-
Play Games

這是一篇總結Java多線程開發的長文。文章是從Java創建之初就存在的synchronized關鍵字引入,對Java多線程和併發模型進行了探討。希望通過此篇內容的解讀能幫助Java開發者更好的理清Java併發編程的脈絡。 互聯網上充斥著對Java多線程編程的介紹,每篇文章都從不同的角度介紹並總結了該 ...


這是一篇總結Java多線程開發的長文。文章是從Java創建之初就存在的synchronized關鍵字引入,對Java多線程和併發模型進行了探討。希望通過此篇內容的解讀能幫助Java開發者更好的理清Java併發編程的脈絡。

互聯網上充斥著對Java多線程編程的介紹,每篇文章都從不同的角度介紹並總結了該領域的內容。但大部分文章都沒有說明多線程的實現本質,沒能讓開發者真正“過癮”。

本篇內容從Java的線程安全鼻祖內置鎖介紹開始,讓你瞭解內置鎖的實現邏輯和原理以及引發的性能問題,接著說明瞭Java多線程編程中鎖的存在是為了保障共用變數的線程安全使用。下麵讓我們進入正題。

以下內容如無特殊說明均指代Java環境。

第一部分:鎖

提到併發編程,大多數Java工程師的第一反應都是synchronized關鍵字。這是Java在1.0時代的產物,至今仍然應用於很多的項目中,伴隨著Java的版本更新已經存在了20多年。在如此之長的生命周期中,synchronized內部也在進行著“自我”進化。

早期的synchronized關鍵字是Java併發問題的唯一解決方案, 伴隨引入這種“重量型”鎖,帶來的性能開銷也是很大的,早期的工程師為瞭解決性能開銷問題,想出了很多解決方案(例如DCL)來提升性能。好在Java1.6提供了鎖的狀態升級來解決這種性能消耗。一般通俗的說Java的鎖按照類別可以分為類鎖和對象鎖兩種,兩種鎖之間是互不影響的,下麵我們一起看下這兩種鎖的具體含義。

類鎖和對象鎖

由於JVM記憶體對象中需要對兩種資源進行協同以保證線程安全,JVM堆中的實例對象和保存在方法區中的類變數。因此Java的內置鎖分為類鎖和對象鎖兩種實現方式實現。前面已經提到類鎖和對象鎖是相互隔離的兩種鎖,它們之間不存在相互的直接影響,以不同方式實現對共用對象的線程安全訪問。下麵根據兩種鎖的隔離方式做如下說明:

1、當有兩個(或以上)線程共同去訪問一個Object共用對象時,同一時刻只有一個線程可以訪問該對象的synchronized(this)同步方法(或同步代碼塊),也就是說,同一時刻,只能有一個線程能夠得到CPU的執行,另一個線程必須等待當前獲得CPU執行的線程完成之後才有機會獲取該共用對象的鎖。

2、當一個線程已經獲得該Object對象的同步方法(或同步代碼塊)的執行許可權時,其他的線程仍然可以訪問該對象的非synchronized方法。

3、當一個線程已經獲取該Object對象的synchronized(this)同步方法(或代碼塊)的鎖時,該對象被類鎖修飾的同步方法(或代碼塊)仍然可以被其他線程在同一CPU周期內獲取,兩種鎖不存在資源競爭情況。

在我們對內置鎖的類別有了基本瞭解後,我們可能會想JVM是如何實現和保存內置鎖的狀態的,其實JVM是將鎖的信息保存在Java對象的對象頭中。首先我們看下Java的對象頭是怎麼回事。

Java對象頭

為瞭解決早期synchronized關鍵字帶來的鎖性能開銷問題,從Java1.6開始引入了鎖狀態的升級方式用以減輕1.0時代鎖帶來的性能消耗,對象的鎖由無鎖狀態 -> 偏向鎖 -> 輕量級鎖 -> 重量級鎖狀的升級。

圖1.1:對象頭

在Hotspot虛擬機中對象頭分為兩個部分(數組還要多一部分用於存儲數組長度),其中一部分用來存儲運行時數據,如HashCode、GC分代信息、鎖標誌位,這部分內容又被稱為Mark Word。在虛擬機運行期間,JVM為了節省存儲成本會對Mark Word的存儲區間進行重用,因此Mark Word的信息會隨著鎖狀態變化而改變。另外一部分用於方法區的數據類型指針存儲。

Java的內置鎖的狀態升級實現是通過替換對象頭中的Mark Word的標識來實現的,下麵具體看下內置鎖的狀態是如何從無鎖狀態升級為重量級鎖狀態。

內置鎖的狀態升級

JVM為了提升鎖的性能,共提供了四種量級的鎖。級別從低到高分為:無狀態的鎖、偏向鎖、輕量級的鎖和重量級的鎖。在Java應用中加鎖大多使用的是對象鎖,對象鎖隨著線程競爭的加劇,最終可能會升級為重量級的鎖。鎖可以升級但不能降級(也就是為什麼我們進行任何基準測試都需要對數據進行預熱,以防止雜訊的干擾,當然雜訊還可能是其他原因)。在說明內置鎖狀態升級之前,先介紹一個重要的鎖概念,自旋鎖。

自旋鎖

在互斥(mutex)狀態下的內置鎖帶來的性能下降是很明顯的。沒有得到鎖的線程需要等待持有鎖的線程釋放鎖才可以爭搶運行,掛起和恢復一個線程的操作都需要從操作系統的用戶態轉到內核態來完成。然而CPU為保障每個線程都能得到運行,分配的時間片是有限的,每次上下文切換都是非常浪費CPU的時間片的,在這種條件下自旋鎖發揮了優勢。

所謂自旋,就是讓沒有得到鎖的線程自己運行一段時間,線程自旋是不會引起線程休眠的(自旋會一直占用CPU資源),所以並不是真正的阻塞。當線程狀態被其他線程改變才會進入臨界區,進而被阻塞。在Java1.6版本已經預設開啟了該設置(可以通過JVM參數-XX:+UseSpinning開啟,在Java1.7中自旋鎖的參數已經被取消,不再支持用戶配置而是虛擬機總會預設執行)。

雖然自旋鎖不會引起線程的休眠,減少了等待時間,但自旋鎖也存在著對CPU資源浪費的情況,自旋鎖需要在運行期間空轉CPU的資源。只有當自旋等待的時間高於同步阻塞時才有意義。因此JVM限制了自旋的時間限度,當超過這個限度時,線程就會被掛起。

在Java1.6 中提供了自適應自旋鎖,優化了原自旋鎖限度的次數問題,改為由自旋線程時間和鎖的狀態來確定。例如,如果一個線程剛剛自旋成功獲取到鎖,那麼下次獲取鎖的可能性就會很大,所以JVM准許自旋的時間相對較長,反之,自旋的時間就會很短或者忽略自旋過程,這種情況在Java1.7也得到了優化。

自旋鎖是貫穿內置鎖狀態始終的,作為偏向鎖,輕量級鎖以及重量級鎖的補充。

偏向鎖

偏向鎖是Java1.6 提出的一種鎖優化機制,其核心思想是,如果當前線程沒有競爭則取消之前已經取得鎖的線程同步操作,在JVM的虛擬機模型中減少對鎖的檢測。也就是說如果某個線程取得對象的偏向鎖,那麼當這個線程在此請求該偏向鎖時,就不需要額外的同步操作了。

具體的實現為當一個線程訪問同步塊時會在對象頭的Mark Word中存儲鎖的偏向線程ID,後續該線程訪問該鎖時,就可以簡單的檢查下Mark Word是否為偏向鎖並且其偏向鎖是否指向當前線程。

如果測試成功則線程獲取到偏向鎖,如果測試失敗,則需要檢測下Mark Word中偏向鎖的標記是否設置成了偏向狀態(標記位為1)。如果沒有設置,則使用CAS競爭鎖。如果設置了,嘗試使用CAS將對象頭的Mark Word偏向鎖標記指向當前線程。也可以使用JVM參數-XX:-UseBiastedLocking參數來禁用偏向鎖。

因為偏向鎖使用的是存在競爭才釋放鎖的機制,所以當其他線程嘗試競爭偏向鎖時,持有偏向鎖的線程才會釋放鎖。

輕量級的鎖

如果偏向鎖獲取失敗,那麼JVM會嘗試使用輕量級鎖,帶來一次鎖的升級。輕量級鎖存在的出發點是為了優化鎖的獲取方式,在不存在多線程競爭的前提下,以減少Java 1.0時代鎖互斥帶來的性能開銷。輕量級鎖在JVM內部是使用BasicObjectLock對象實現的。

其具體的實現為當前線程在進入同步代碼塊之前,會將BasicObjectLock對象放到Java的棧楨中,這個對象的內部是由BasicLock對象和該Java對象的指針組成的。然後當前線程嘗試使用CAS替換對象頭中的Mark Word鎖標記指向該鎖記錄指針。如果成功則獲取到鎖,將對象的鎖標記改為00 | locked,如果失敗則表示存在其他線程競爭,當前線程使用自旋嘗試獲取鎖。

當存在兩條(或以上)的線程共同競爭一個鎖時,此時的輕量級的鎖將不再發揮作用,JVM會將其膨脹為重量級的鎖,鎖的標位為也會修改為10 | monitor 。

輕量級鎖在解鎖時,同樣是通過CAS的置換對象頭操作。如果成功,則表示成功獲取到鎖。如果失敗,則說明該對象存在其他線程競爭,該鎖會隨著膨脹為重量級的鎖。

重量級的鎖

JVM在輕量級鎖獲取失敗後,會使用重量級的鎖來處理同步操作,此時對象的Mark Word標記為 10 | monitor,在重量級鎖處理線程的調度中,被阻塞的線程會被系統掛起,線上程再次獲得CPU資源後,需要進行系統上下文的切換才能得到CPU執行,此時效率會低很多。

通過上面的介紹我們瞭解了Java的內置鎖升級策略,隨著鎖的每次升級帶來的性能的下降,因此我們在程式設計時應該儘量避免鎖的徵用,可以使用集中式緩存來解決該問題。

一個小插曲:內置鎖的繼承

內置鎖是可以被繼承的,Java的內置鎖在子類對父類同步方法進行方法覆蓋時,其同步標誌是可以被子類繼承使用的,我們看下麵的例子:

public class Parent { 
public synchronized void doSomething() { 
     System.out.println("parent do something"); 
} 
} 
 java學習群669823128
public class Child extends Parent { 
public synchronized void doSomething() { 
.doSomething(); 
} 
 
public static void main(String[] args) { 
     new Child().doSomething(); 
} 
} 

代碼1.1:內置鎖繼承

以上的代碼可以正常的運行麽?

答案是肯定的。

避免活躍度危險

Java併發的安全性和活躍度是相互影響的,我們使用鎖來保障線程安全的同時,需要避免線程活躍度的風險。Java線程不能像資料庫那樣自動排查解除死鎖,也無法從死鎖中恢復。而且程式中死鎖的檢查有時候並不是顯而易見的,必須到達相應的併發狀態才會發生,這個問題往往給應用程式帶來災難性的結果,這裡介紹以下幾種活躍度危險:死鎖、線程饑餓、弱響應性、活鎖。

死鎖

當一個線程永遠的占有一個鎖,而其他的線程嘗試去獲取這個鎖時,這個線程將被永久的阻塞。

一個經典的例子就是AB鎖問題,線程1獲取到了共用數據A的鎖,同時線程2獲取到了共用數據B的鎖,此時線程1想要去獲取共用數據B的鎖,線程2獲取共用數據A的鎖。如果用圖的關係表示,那麼這將是一個環路。這是死鎖是最簡單的形式。還有比如我們再對批量無序的數據做更新操作時,如果無序的行為引發了2個線程的資源爭搶也會引發該問題,解決的途徑就是排序後再進行處理。

線程饑餓

線程饑餓是指當線程訪問它所需要的資源時卻永久被拒絕,以至於不能再繼續進行後面的流程,這樣就發生了線程饑餓;例如線程對CPU時間片的競爭,Java中低優先順序的線程引用不當等。雖然Java的API中對線程的優先順序進行了定義,這僅僅是一種向CPU自我推薦的行為(此處需要註意不同操作系統的線程優先順序並不統一,而且對應的Java線程優先順序也不統一),但是這並不能保障高優先順序的線程一定能夠先被CPU選擇執行。

弱響應性

在GUI的程式中,我們一般可見的客戶端程式都是使用後臺運行,前端反饋的形式,當CPU密集型後臺任務與前臺任務共同競爭資源時,有可能造成前端GUI凍結的效果,因此我們可以降低後臺程式的優先順序,儘可能的保障最佳的用戶體驗性。

活鎖

線程活躍度失敗的另一種體現是線程沒有被阻塞,但是卻不能繼續,因為不斷重試相同的操作,卻總是失敗。

線程的活躍度危險是我們在開發中應該避免的一種行為。這種行為會造成應用程式的災難性後果。 

總結

關於synchronized關鍵字的所有內容到這裡全部介紹完畢了,在這一章節希望可以讓大家明白鎖之所以“重”是因為隨著線程間競爭的程度升級導致的。在真正的開發中我們可能還有別的選擇,例如Lock介面,在某些併發場景下性能優於內置鎖的實現。

不論是通過內置鎖還是通過Lock介面都是為了保障併發的安全性,併發環境一般需要考慮的問題是如何保障共用對象的安全訪問。在第二章將詳細介紹內置對象引發的線程安全問題以及解決之道。

java學習群669823128


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

-Advertisement-
Play Games
更多相關文章
  • #00 在python裡面沒有常量,只有變數 #01刪除變數:del 變數名 #02coding:utf-8 //設置utf-8編碼 #03 單行註釋:#多行註釋:三個任意符號,一般用'''#04 table鍵和四個空格不相等,縮進要保持一致 我的IDLE中預設按tab鍵就是四個空格 ...
  • 前言 接手前輩的項目,沒有接觸、安裝、使用過perl和DBD::Oracle,也沒有相關的文檔記錄,茫茫然不知所措~~。一開始發現這個問題,就想著迅速解決,就直接在google上搜報錯信息,搜索的過程中發現 如果不搞清楚前因後果我連解決方案都‘看不見’‘看不懂’。 所以還是要補充這方面的知識,再思考 ...
  • 一 文件操作 1.介紹 電腦系統分為:電腦硬體,操作系統,應用程式三部分。 我們用python或其他語言編寫的應用程式若想要把數據永久保存下來,必須要保存於硬碟中,這就涉及到應用程式要操作硬體,眾所周知,應用程式是無法直接操作硬體的,這就用到了操作系統。操作系統把複雜的硬體操作封裝成簡單的介面給 ...
  • 鏈表可以說是一種最為基礎的數據結構。鏈表由一組元素以一種特定的順序組合或鏈接而成,在維護數據的集合時很有用。這一點同我們常用的數組很相似。然而,鏈表在很多情況下比數組更有優勢。特別是在執行插入和刪除操作時鏈表擁有更高的效率。鏈表需要動態的開闢存儲空間,也就是存儲空間是在程式運行時分配的。由於在很多應... ...
  • 好久沒來這個站了,我覺得不能這麼太監了。 現在正在用Python開發一個網站,地址在(h2magic.xyz),建設正在進行時,也許這個站都沒了也說不定。現在什麼都沒有穩定下來。 ...
  • 在visual studio的工程項目應用中打開console控制視窗,這個可以方便我們在console中輸出參數的值檢查錯誤。 只需要在需要打開console的地方加入下麵的代碼即可。 ...
  • Django是一個重量級的web開發框架,它提供了很多內部已開發好的插件供我們使用;這裡不去描述 Django直接進入開發過程。 Django入門案例分兩部分:一、開發環境的配置;二、業務需求分析。 第一部分:開發環境的配置 開發環境藍圖 >> (1).setting.py配置: 配置資料庫連接: ...
  • 隨筆背景:在很多時候,很多入門不久的朋友都會問我:我是從其他語言轉到程式開發的,有沒有一些基礎性的資料給我們學習學習呢,你的框架感覺一下太大了,希望有個循序漸進的教程或者視頻來學習就好了。對於學習有困難不知道如何提升自己可以加扣:1225462853進行交流得到幫助,獲取學習資料. CL1338-S ...
一周排行
    -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.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...