Java 併發編程解析 | 如何正確理解Java領域中的併發鎖,我們應該具體掌握到什麼程度?

蒼穹之邊,浩瀚之摯,眰恦之美; 悟心悟性,善始善終,惟善惟道! —— 朝槿《朝槿兮年說》

Picture-Navigation

寫在開頭

Picture-Header

對於Java領域中的鎖,其實從接觸Java至今,我相信每一位Java Developer都會有這樣的一個感覺?不論是Java對鎖的實現還是應用,真的是一種“羣英薈萃”,而且每一種鎖都有點各有各的驢,各有各的本,各不相同。

在很多情況下,以及在各種鎖的應用場景裏,各式各樣的定義,難免會讓我們覺得無所適從,很難清楚該如何對這些鎖做到得心應手?

在併發編程色世界中,一般情況下,我們只需瞭解其是如何使用鎖之後就已經滿足我們大部分的需求,但是作爲一名對技術研究有執念和熱情的人來說,深入探究和分析纔是對技術的探祕之樂趣。

作爲一名Java Developer來說,深入探究和分析和正確瞭解和掌握這些鎖的機制和原理,需要我們帶着一些實際問題,通過對其探究分析和加上實際應用分析,才能真正意義上理解和掌握。

一般來說,針對於不同場景提供的鎖,都用於解決什麼問題?不論是從實現方式上,還是從使用場景上,都可以應對這些鎖的特點,我們又該如何認識和理解?

接下來,今天我們就一起來盤一盤,Java領域中那些併發鎖,盤點一下相關的鎖,從設計基本思想和設計實現,以及應用分析等方面來總體分析探討一下。

關健術語

Picture-Keyword

本文用到的一些關鍵詞語以及常用術語,主要如下:

  • 進程(Process): 計算機中的程序關於某數據集合上的一次運行活動,是系統進行資源分配和調度的基本單位,是操作系統結構的基礎。在早期面向進程設計的計算機結構中,進程是程序的基本執行實體;在當代面向線程設計的計算機結構中,進程是線程的容器。
  • 線程(thread): 操作系統能夠進行運算調度的最小單位。它被包含在進程之中,是進程中的實際運作單位。在Unix System V及SunOS中也被稱爲輕量進程(Light-Weight Processes),但輕量進程更多指內核線程(Kernel Thread),而把用戶線程(User Thread)稱爲線程。

基本概述

Picture-Content

在Java領域中,單純從Java對其實現的方式上來看,我們大體上可以將其分爲基於Java語法層面(關鍵詞)實現的鎖和基於JDK層面實現的鎖。

基於這兩個基本點,可以作爲我們對於Java領域中的鎖的一個基礎認識,這對於我們認識和了解Java領域中的鎖指導一個參考方向。

一般來說,鎖是併發編程中最基礎和最常用的一項技術,而且在Java的內部JDK中其使用也是非常地廣泛。

接下來,我們便一起探究和認識一下Java領域中的各種各樣的鎖。

一.鎖的基本理論

鎖的基本理論主要是指從鎖的基本定義和基本特點以及基本意義去分析的一般模型理論,是一套幫助我們認識和了解鎖的簡單的思維方法論。

Picture-Content

一般在瞭解一個事物之前,我們都會按照基本定義,基本特點以及基本意義去看待這個事物。在計算機的世界裏,鎖本身也和我們實際生活一樣,也是一個比較普遍且應用場景繁多的一種事物。

比如,在操作系統中,也定義了各種各樣的鎖;在數據庫系統中也出現了鎖。甚至,在CPU處理器架構中都會看見鎖的身影。

但是,這裏就會有一個問題:既然都在使用鎖,可是對於鎖該去如何定義,似乎都很難給出一個準確的定義? 換而言之,這也許就是我們對於鎖只是知道有這個東西,但是一直有云裏霧裏的基本原因。

從本質上講,計算機軟件開發領域中的鎖是一種協調多個進程 或者多個線程對某一個資源的訪問的控制機制,其核心是作用於資源,也作用於着這個定義中提到的進程和線程等。其中:

vTz8X9.png

  • 進程(Process): 操作系統進行資源分配和調度的基本單位,是計算機程序中的實體,其中,程序是指令、數據及其組織形式的描述。
  • 線程(Thread) : 操作系統能夠進行運算調度的最小單位,一條線程指的是進程中一個單一順序的控制流,一個進程中可以併發多個線程,每條線程並行執行不同的任務。

一般來說,線程主要分爲位於系統內核空間的線程稱爲內核線程(Kernel Thread)和位於應用程序的用戶空間的線程被稱爲用戶線程(User Thread)兩種,其中:

v8MXh4.png

也就是我們一般說的Java線程等均屬於用戶線程,而內核線程主要是操作系統封裝的函數庫以及API等。

而且最關健的就是,我們平日裏所提到Java線程和JVM都是位於用戶空間之中,從Java層到操作系統系統的線程調度順序來看,一般流程是:java.lang.Thread(Target Thread)->Java Thread->OSThread->pthread->Kernel Thread。

v7plL9.png

簡單來說,在Java領域中,鎖是用於控制多個線程訪問共享資源的工具。一般,鎖提供對共享資源的獨立訪問:一次只有一個線程可以獲取鎖,所有對共享資源的訪問都需要先獲取鎖。但是,某些鎖可以併發訪問共享資源。

對於併發訪問共享資源來說,主要是依據現在大多數操作系統的線程的調度方式是搶佔式調度,因此加鎖是爲了維護數據的一致性和完整性,其實就是數據的安全性。

綜上所述,我們便可以得到一個關於鎖的基本概念模型,接下來我們便來一一盤點以下主要有哪些鎖。

二.鎖的基本分類

在Java領域中,我們可以將鎖大致分爲基於Java語法層面(關鍵詞)實現的鎖和基於JDK層面實現的鎖。

v7iA6U.png

單純從Java對其實現的方式上來看,我們大體上可以將其分爲基於Java語法層面(關鍵詞)實現的鎖和基於JDK層面實現的鎖。其中:

  • Java內置鎖:基於Java語法層面(關鍵詞)實現的鎖,主要是根據Java語義來實現,最典型的應用就是synchronized。
  • Java顯式鎖:基於JDK層面實現的鎖,主要是根據基於Lock接口和ReadWriteLock接口,以及統一的AQS基礎同步器等來實現,最典型的有ReentrantLock。

需要特別注意的是,在Java領域中,基於JDK層面的鎖通過CAS操作解決了併發編程中的原子性問題,而基於Java語法層面實現的鎖解決了併發編程中的原子性問題和可見性問題。

除此之外之外,在Java併發容器中曾用到過一種Segment數組結構來實現的分段鎖。

而從具體到對應的Java線程資源來說,我們按照是否含有某一特性來定義鎖,主要可以從如下幾個方面來看:

  • 從加鎖對象角度方面上來看,線程要不要鎖住同步資源 ? 如果是需要加鎖,鎖住同步資源的情況下,一般稱其爲悲觀鎖;否則,如果是不需要加鎖,且不用鎖住同步資源的情況就屬於爲樂觀鎖。
  • 從獲取鎖的處理方式上來看,假設鎖住同步資源,其對該線程是否進入睡眠狀態或者阻塞狀態?如果會進入睡眠狀態或者阻塞狀態,一般稱其爲互斥鎖,否則,不會進入睡眠狀態或者阻塞狀態屬於一種非阻塞鎖,即就是自旋鎖。
  • 從鎖的變化狀態方面來看,多個線程在競爭資源的流程細節上是否有差別?
  • 首先,對於不會鎖住資源,多個線程只有一個線程能修改資源成功,其他線程會依據實際情況進行重試,即就是不存在競爭的情況,一般屬於無鎖。
  • 其次,對於同一個線程執行同步資源會自動獲取鎖資源,一般屬於偏向鎖。
  • 然而,對於多線程競爭同步資源時,沒有獲取到鎖資源的線程會自旋等待鎖釋放,一般屬於輕量級鎖。
  • 最後,對於多線程競爭同步資源時,沒有獲取到鎖資源的線程會阻塞等待喚醒,一般屬於重量級鎖。
  • 從鎖競爭時公平性上來看,多個線程在競爭資源時是否需要排隊等待?如果是需要排隊等待的情況,一般屬於公平鎖;否則,先插隊,然後再嘗試排隊的情況屬於非公平鎖。
  • 從獲取鎖的操作頻率次數來看,一個線程中的多個流程是否可以獲取同一把鎖?如果是可以多次進行加鎖操作的情況,一般屬於可重入鎖,否則,可以多次進行加鎖操作的情況屬於非可重入鎖。
  • 從獲取鎖的佔有方式上來看,多個線程能不能共享一把鎖?如果是可以共享鎖資源的情況,一般屬於共享鎖;否則,獨佔鎖資源的情況屬於排他鎖。

針對於上述描述的各種情況,接下來,我們便來一起詳細看看,在Java領域中,這個鎖的具體情況。

三.Java內置鎖

在Java領域中,Java內置鎖主要是指基於Java語法層面(關鍵詞)實現的鎖。

v7EYxP.png

在Java領域中,我們把基於Java語法層面(關鍵詞)實現的鎖稱爲內置鎖,比如synchronized 關鍵字。

對於synchronized 關鍵字的解釋,最直接的就是Java語言中爲開發人員提供的同步工具,可以看作是Java中的一種“語法糖”。主要宗旨在於解決多線程併發執行過程中數據同步的問題。

不像其他的編程語言(C++),在處理同步問題時都需要自己進行鎖處理,主要特點就是簡單,直接聲明即可。

在 Java 程序中,利用 synchronized 關鍵字來對程序進行加鎖,其實現同步的語義是互斥鎖。既可以用來聲明一個 synchronized 代碼塊,也可以直接標記靜態方法或者實例方法。

其中,對於互斥的概念來說,在數學範疇來講,是一個數學名詞,表示和描述的是事件A與事件B在任何一次試驗中都不會同時發生,則稱事件A與事件B互斥。

因此,對於互斥鎖可以理解爲: 對於某一個鎖來說,任意時刻只能有一個線程獲得該鎖,對於其他線程想獲取鎖的時候就得等待或者被阻塞。

1.使用方式

在Java領域中,synchronized關鍵字互斥鎖主要有作用於對象方法上面,作用於類靜態方法上面,作用於對象方法裏面,作用於類靜態方法裏面等4種方式。

v7Eha4.png

在Java領域中,synchronized關鍵字從使用方式來看,主要可以分爲:

  • 作用於對象方法上面:
    • 描述對象的方法,表示該對象的方法具有同步性。由於描述的對象的方法,作用範圍是在對象(Object),整個對象充當了鎖。
    • 需要注意的是,類可以實例化多個對象,這時每一個對象都是一個鎖,每個鎖的範圍相當於是當前對象來說的。
  • 作用於類靜態方法上面:
    • 描述類的靜態方法,表示該方法具有同步性。由於描述的類靜態的方法,作用範圍是在類(Class),整個類充當了鎖。
    • 需要注意的是,某一個類的本身也是一個對象,JVM使用這個對象作爲模板去生成該類的對象時,每個鎖的範圍相當於是當前類來說的。
  • 作用於對象方法裏面:
    • 描述方法內部的某塊邏輯,表示該代碼塊具有同步性。
    • 需要注意的是,一般需要我們指定對象,比如synchronized(this){xxx}是指當前對象的,也可以創建一個對象來作爲鎖。
  • 作用於類靜態方法裏面:
    • 描述靜態方法內部的某塊邏輯,表示該代碼塊具有同步性。
    • 需要注意的是,一般需要我們指定鎖對象,比如synchronized(this){xxx}是指當前類class作爲鎖對象的,也可以創建一個對象來作爲鎖。

一般當我們在編寫代碼的過程中,如果按照上述方式聲明時,被synchronized關鍵字聲明的代碼會比普通代碼在編譯之後,使用javap -c xxx.class 查看字節碼,就會發現多兩個monitorenter和monitorexit指令。

2.基本思想

在Java領域中,synchronized關鍵字互斥鎖主要基於一個阻塞隊列和等待對列,類似於一種“等待-通知”的工作機制來實現。

v7EoGR.png

一般情況下,“等待 - 通知”的工作機制的要求是線程首先獲取互斥鎖,其中:

  • 當線程要求的條件不滿足時,釋放互斥鎖,進入等待狀態。
  • 當要求的條件滿足時,通知等待的線程,重新獲取互斥鎖。

在Java領域中, Java 語言內置的 synchronized 配合java.lang.Object類定義的 wait()、notify()、notifyAll() 這三個方法就能輕鬆實現等待 - 通知機制,其中:

  • wait: 表示持有對象鎖的線程A準備釋放對象鎖權限,釋放cpu資源並進入等待。
  • notify:表示持有對象鎖的線程A準備釋放對象鎖權限,通知jvm喚醒某個競爭該對象鎖的線程X。線程A synchronized 代碼作用域結束後,線程X直接獲得對象鎖權限,其他競爭線程繼續等待(即使線程X同步完畢,釋放對象鎖,其他競爭線程仍然等待,直至有新的notify ,notifyAll被調用)。
  • 表示持有對象鎖的線程A準備釋放對象鎖權限,通知jvm喚醒所有競爭該對象鎖的線程,線程A synchronized 代碼作用域結束後,jvm通過算法將對象鎖權限指派給某個線程X,所有被喚醒的線程不再等待。線程X synchronized代碼作用域結束後,之前所有被喚醒的線程都有可能獲得該對象鎖權限,這個由JVM算法決定。

一個線程一旦調用了任意對象的wait()方法,就會變爲非運行狀態,直到另一個線程調用了同一個對象的notify()方法。

爲了調用wait()或者notify(),線程必須先獲得那個對象的鎖。也就是說,線程必須在同步塊裏調用wait()或者notify()。

對於等待隊列的工作機制來說,同一時刻,只允許一個線程進入 synchronized 保護的臨界區。當有一個線程進入臨界區後,其他線程就只能進入圖中左邊的等待隊列裏等待。這個等待隊列和互斥鎖是一對一的關係,每個互斥鎖都有自己獨立的等待隊列。在併發程序中,其中:

v7ZYB8.png

  • 當一個線程進入臨界區後,由於某些條件不滿足,需要進入等待狀態,Java 對象的 wait() 方法就能夠滿足這種需求。
  • 當調用 wait() 方法後,當前線程就會被阻塞,並且進入到右邊的等待隊列中,這個等待隊列也是互斥鎖的等待隊列。
  • 線程在進入等待隊列的同時,會釋放持有的互斥鎖,線程釋放鎖後,其他線程就有機會獲得鎖,並進入臨界區了。

對於通知隊列的工作機制來說,那線程要求的條件滿足時,該怎麼通知這個等待的線程呢?很簡單,就是 Java 對象的 notify() 和 notifyAll() 方法。當條件滿足時調用 notify(),會通知等待隊列(互斥鎖的等待隊列)中的線程,告訴它條件曾經滿足過。爲什麼說是曾經滿足過呢?其中:

v7ZaNQ.png

  • 因爲 notify() 只能保證在通知時間點,條件是滿足的。
  • 而被通知線程的執行時間點和通知的時間點基本上不會重合,所以當線程執行的時候,很可能條件已經不滿足了(保不齊有其他線程插隊)。
  • 除此之外,還有一個需要注意的點,被通知的線程要想重新執行,仍然需要獲取到互斥鎖(因爲曾經獲取的鎖在調用 wait() 時已經釋放了)。

上面我們一直強調 wait()、notify()、notifyAll() 方法操作的等待隊列是互斥鎖的等待隊列,其中:

  • 如果 synchronized 鎖定的是 this,那麼對應的一定是 this.wait()、this.notify()、this.notifyAll();
  • 如果 synchronized 鎖定的是 target,那麼對應的一定是 target.wait()、target.notify()、target.notifyAll() 。

而且 wait()、notify()、notifyAll() 這三個方法能夠被調用的前提是已經獲取了相應的互斥鎖,所以我們會發現 wait()、notify()、notifyAll() 都是在 synchronized{}內部被調用的。

如果在 synchronized{}外部調用,或者鎖定的 this,而用 target.wait() 調用的話,JVM 會拋出一個運行時異常:java.lang.IllegalMonitorStateException。

對於和notifyAll() 和notify()來實現通知機制,特別需要注意的是,兩者之間的區別:

  • notify()方法 : 隨機地通知等待隊列中的一個線程。
  • notifyAll()方法: 通知等待隊列中的所有線程。

從感覺上來講,應該是 notify() 更好一些,因爲即便通知所有線程,也只有一個線程能夠進入臨界區。但是實際上使用 notify() 也很有風險,主要在於可能導致某些線程永遠不會被通知到。

在具體使用過程中,所以除非經過深思熟慮,一般推薦儘量使用 notifyAll()。

3.基本實現

在Java領域中,synchronized關鍵字互斥鎖主要基於Java HotSpot(TM) VM 虛擬機通過Monitor(監視器)來實現monitorenter和monitorexit指令的。

v7E7xx.png

在Java HotSpot(TM) VM 虛擬機中,主要是通過Monitor(監視器)來實現monitorenter和monitorexit指令的,Monitor(監視器)一般包括一個阻塞隊列和一個等待隊列,其中:

  • 阻塞隊列 : 用來保存鎖競爭失敗的線程,它們處於阻塞狀態。
  • 等待隊列:用來保持synchronized關鍵字塊中的調用 wait()方法後放置的隊列。

其中,需要注意的是,當調用 wait()方法後會釋放鎖並通知阻塞隊列。

一般來說,當Java字節碼(class)被託管到Java HotSpot(TM) VM 虛擬機後,Monitor(監視器)就被採用ObjectMonitor接管,其中:

  • 每⼀個對象都有⼀個屬於⾃⼰的monitor,其次如果線程未獲取到singal (許可),則線程阻塞。
  • monitor相當於⼀個對象的鑰匙,只有拿到此對象的monitor,才能訪問該對象的同步代碼。 相反未獲得monitor的只能阻塞來等待持有monitor的線程釋放monitor。

對於monitorenter指令來說,其中:

  • ⼀個對象都會和⼀個監視器monitor關聯。監視器被佔⽤時會被鎖住,其他線程⽆法來獲取該monitor。當JVM執⾏某個線程的某個⽅法內部的monitorenter時,它會嘗試去獲取當前對象對應的monitor的所有權。
  • synchronized的鎖對象會關聯⼀個monitor,這個monitor不是我們主動創建的,是JVM的線程執⾏到這個同步代碼塊,發現鎖對象沒有monitor就會創建monitor,monitor內部有兩個重要的成員變量owner:擁有這把鎖的線程,recursions會記錄線程擁有鎖的次數,當⼀個線程擁有monitor後其他線程只能等待。

主要工作流程如下:

  • 若monior的進⼊數爲0,線程可以進⼊monitor,進入後將monitor的進數置爲1。當前線程成爲monitor的owner(所有者) 。
  • 若線程已擁有monitor的所有權,允許它重⼊monitor,則進⼊monitor的進⼊數再加1。
  • 若其他線程已經佔有monitor的所有權,那麼當前嘗試獲取monitor的所有權的線程會被阻塞。直到monitor的進⼊數變爲0,才能重新嘗試獲取monitor的所有權。

對於monitorexit指令來說,其中:

  • 能執⾏monitorexit指令的線程,⼀定是擁有當前對象的monitor的所有權的線程。
  • 執⾏monitorexit時會將monitor的進⼊數減1。當monitor的進⼊數減爲0時,當前線程退出monitor,不再擁有monitor的所有權,此時其他被這個monitor阻塞的線程可以嘗試去獲取這個monitor的所有權。

主要工作流程如下:

  • monitorexit,指令出現了兩次,第1次爲同步正常退出釋放鎖;第2次爲發生異常退出釋放鎖。
  • monitorexit釋放鎖monitorexit插⼊在⽅法結束處和異常處,JVM保證每個monitorenter必須有對應的monitorexit。

綜上所述,monitorenter和monitorexit兩個指令的執行是JVM通過調用操作系統的互斥原語mutex來實現。被阻塞的線程會被掛起、等待重新調度,會導致"用戶態和內核態"兩個態之間來回切換,對性能有較大影響。

4.具體實現

在Java領域中,JVM中每個對象都會有一個監視器,監視器和對象一起創建、銷燬。監視器相當於一個用來監視這些線程進入的特殊房間,其義務是保證(同一時間)只有一個線程可以訪問被保護的臨界區代碼塊。

v7MaNT.png

本質上,監視器是一種同步工具,也可以說是一種同步機制,主要特點是:

  • 同步:監視器所保護的臨界區代碼是互斥地執行的。一個監視器是一個運行許可,任一線程進入臨界區代碼都需要獲得這個許可,離開時把許可歸還。
  • 協作:監視器提供Signal機制,允許正持有許可的線程暫時放棄許可進入阻塞等待狀態,等待其他線程發送Signal去喚醒;其他擁有許可的線程可以發送Signal,喚醒正在阻塞等待的線程,讓它可以重新獲得許可並啓動執行。

在Hotspot虛擬機中,監視器是由C++類ObjectMonitor實現的,ObjectMonitor類定義在ObjectMonitor.hpp文件中,ObjectMonitor的Owner(_owner)、WaitSet(_WaitSet)、Cxq(_cxq)、EntryList(_EntryList)這幾個屬性比較關鍵。

v7MB34.png

ObjectMonitor的WaitSet、Cxq、EntryList這三個隊列存放搶奪重量級鎖的線程,而ObjectMonitor的Owner所指向的線程即爲獲得鎖的線程。其中:

  • Cxq:競爭隊列(Contention Queue),所有請求鎖的線程首先被放在這個競爭隊列中
  • EntryList:Cxq中那些有資格成爲候選資源的線程被移動到EntryList中。
  • WaitSet:某個擁有ObjectMonitor的線程在調用Object.wait()方法之後將被阻塞,然後該線程將被放置在WaitSet鏈表中。

Cxq並不是一個真正的隊列,只是一個虛擬隊列,原因在於Cxq是由Node及其next指針邏輯構成的,並不存在一個隊列的數據結構。每次新加入Node會在Cxq的隊頭進行,通過CAS改變第一個節點的指針爲新增節點,同時設置新增節點的next指向後續節點;從Cxq取得元素時,會從隊尾獲取。顯然,Cxq結構是一個無鎖結構。
在線程進入Cxq前,搶鎖線程會先嚐試通過CAS自旋獲取鎖,如果獲取不到,就進入Cxq隊列,這明顯對於已經進入Cxq隊列的線程是不公平的。所以,synchronized同步塊所使用的重量級鎖是不公平鎖。

EntryList與Cxq在邏輯上都屬於等待隊列。Cxq會被線程併發訪問,爲了降低對Cxq隊尾的爭用,而建立EntryList。在Owner線程釋放鎖時,JVM會從Cxq中遷移線程到EntryList,並會指定EntryList中的某個線程(一般爲Head)爲OnDeck Thread(Ready Thread)。EntryList中的線程作爲候選競爭線程而存在。

JVM不直接把鎖傳遞給Owner Thread,而是把鎖競爭的權利交給OnDeck Thread,OnDeck需要重新競爭鎖。這樣雖然犧牲了一些公平性,但是能極大地提升系統的吞吐量,在JVM中,也把這種選擇行爲稱爲“競爭切換”。
OnDeck Thread獲取到鎖資源後會變爲Owner Thread。無法獲得鎖的OnDeck Thread則會依然留在EntryList中,考慮到公平性,OnDeck Thread在EntryList中的位置不發生變化(依然在隊頭)。
在OnDeck Thread成爲Owner的過程中,還有一個不公平的事情,就是後來的新搶鎖線程可能直接通過CAS自旋成爲Owner而搶到鎖。

如果Owner線程被Object.wait()方法阻塞,就轉移到WaitSet隊列中,直到某個時刻通過Object.notify()或者Object.notifyAll()喚醒,該線程就會重新進入EntryList中。

處於ContentionList、EntryList、WaitSet中的線程都處於阻塞狀態,線程的阻塞或者喚醒都需要操作系統來幫忙,Linux內核下采用pthread_mutex_lock系統調用實現,進程需要從用戶態切換到內核態。

5.基本分類

在Java領域中,synchronized關鍵字互斥鎖主要中內置鎖一共有4種狀態:無鎖狀態、偏向鎖狀態、輕量級鎖狀態和重量級鎖狀態,這些狀態隨着競爭情況逐漸升級。

v7EqsK.png

在Java領域中,一般Java對象(Object實例)結構包括三部分:對象頭、對象體和對齊字節,其中:

v7h8kn.png

  • 對象頭(Object Header) :對象頭包括三個字段,主要是作Mark Word(標記字段)、Klass Pointer(類型指針)以及Array Length(數組長度)等。
  • 對象體(Object Data) :包含對象的實例變量(成員變量),用於成員屬性值,包括父類的成員屬性值。這部分內存按4字節對齊。
  • 對齊字節(Padding): 也叫作填充對齊,其作用是用來保證Java對象所佔內存字節數爲8的倍數HotSpot VM的內存管理要求對象起始地址必須是8字節的整數倍。

一般地,對象頭本身是8的倍數,當對象的實例變量數據不是8的倍數時,便需要填充數據來保證8字節的對齊。

上面在悲觀鎖和樂觀鎖分類時候,提到synchronized是悲觀鎖, 以Hotspot虛擬機爲例,在操作同步資源之前需要給同步資源先加鎖,這把鎖就是存在Java對象頭裏,其中:

  • Mark Word(標記字段):默認存儲對象的HashCode,分代年齡和鎖標誌位信息。這些信息都是與對象自身定義無關的數據,所以Mark Word被設計成一個非固定的數據結構以便在極小的空間內存存儲儘量多的數據。它會根據對象的狀態複用自己的存儲空間,也就是說在運行期間Mark Word裏存儲的數據會隨着鎖標誌位的變化而變化。
  • Klass Pointer(類型指針):對象指向它的類元數據的指針,虛擬機通過這個指針來確定這個對象是哪個類的實例。

而對於synchronized來說,synchronized通過Monitor來實現線程同步,Monitor是依賴於底層的操作系統的Mutex Lock(互斥鎖)來實現的線程同步,主要是通過JVM中Monitor監視器來實現monitorenter和monitorexit指令的,而Monitor可以理解爲一個同步工具或一種同步機制,通常被描述爲一個對象。每一個Java對象就有一把看不見的鎖,稱爲內部鎖或者Monitor鎖。

Monitor是線程私有的數據結構,每一個線程都有一個可用monitor record列表,同時還有一個全局的可用列表。每一個被鎖住的對象都會和一個monitor關聯,同時monitor中有一個Owner字段存放擁有該鎖的線程的唯一標識,表示該鎖被這個線程佔用。

在JDK 1.6版本之前,所有的Java內置鎖都是重量級鎖。重量級鎖會造成CPU在用戶態和核心態之間頻繁切換,所以代價高、效率低。

JDK 1.6版本爲了減少獲得鎖和釋放鎖所帶來的性能消耗,引入了偏向鎖和輕量級鎖的實現。

在JDK 1.6版本中內置鎖一共有4種狀態:無鎖狀態、偏向鎖狀態、輕量級鎖狀態和重量級鎖狀態,這些狀態隨着競爭情況逐漸升級。其中:

  • 無鎖狀態:Java對象剛創建時還沒有任何線程來競爭,說明該對象處於無鎖狀態(無線程競爭它),這時偏向鎖標識位是0,鎖狀態是01。
  • 偏向鎖狀態: 指一段同步代碼一直被同一個線程所訪問,那麼該線程會自動獲取鎖,降低獲取鎖的代價。如果內置鎖處於偏向狀態,當有一個線程來競爭鎖時,先用偏向鎖,表示內置鎖偏愛這個線程,這個線程要執行該鎖關聯的同步代碼時,不需要再做任何檢查和切換。偏向鎖在競爭不激烈的情況下效率非常高。
  • 輕量級鎖狀態:當有兩個線程開始競爭這個鎖對象時,情況就發生變化了,不再是偏向(獨佔)鎖了,鎖會升級爲輕量級鎖,兩個線程公平競爭,哪個線程先佔有鎖對象,鎖對象的Mark Word就指向哪個線程的棧幀中的鎖記錄。
  • 重量級鎖狀態:重量級鎖會讓其他申請的線程之間進入阻塞,性能降低。重量級鎖也叫同步鎖,這個鎖對象Mark Word再次發生變化,會指向一個監視器對象,該監視器對象用集合的形式來登記和管理排隊的線程。

因此,根據上述的鎖狀態來看,我們可以把Java內置鎖分爲無鎖,偏向鎖,輕量級鎖和重量級鎖等4種鎖,其中:

  • 無鎖:表示Java對象實例剛創建,還沒有鎖參與競爭。即就是沒有對資源進行鎖定,所有的線程都能訪問並修改同一個資源,但同時只有一個線程能修改成功。
  • 偏向鎖:偏向鎖主要解決無競爭下的鎖性能問題,所謂的偏向就是偏心,即鎖會偏向於當前已經佔有鎖的線程。指一段同步代碼一直被一個線程所訪問,那麼該線程會自動獲取鎖,降低獲取鎖的代價。
  • 輕量級鎖:輕量級鎖主要有兩種:普通自旋鎖和自適應自旋鎖。當鎖是偏向鎖的時候,被另外的線程所訪問,偏向鎖就會升級爲輕量級鎖,其他線程會通過自旋的形式嘗試獲取鎖,不會阻塞,從而提高性能。由於JVM輕量級鎖使用CAS進行自旋搶鎖,這些CAS操作都處於用戶態下,進程不存在用戶態和內核態之間的運行切換,JVM輕量級鎖開銷較小。
  • 重量級鎖: JVM重量級鎖使用了Linux內核態下的互斥鎖,升級爲重量級鎖時,等待鎖的線程都會進入阻塞狀態,其開銷較大。

從鎖升級的狀態順序來看,只能是: 無鎖->偏向鎖->輕量級鎖->重量級鎖 ,而且順序不可逆,也就是不能降級。

v7WjxS.png

綜上所述,在Java內置鎖中,偏向鎖通過對比Mark Word解決加鎖問題,避免執行CAS操作。而輕量級鎖是通過用CAS操作和自旋來解決加鎖問題,避免線程阻塞和喚醒而影響性能。重量級鎖是將除了擁有鎖的線程以外的線程都阻塞。

6.應用分析

在Java領域中,synchronized關鍵字互斥鎖主要中內置鎖使用簡單,但是鎖的粒度比較大,無法支持超時等。

v7eV8s.png

從synchronized的執行過程,大致如下:

  • 線程搶鎖時,JVM首先檢測內置鎖對象Mark Word中的biased_lock(偏向鎖標識)是否設置成1,lock(鎖標誌位)是否爲01,如果都滿足,確認內置鎖對象爲可偏向狀態。
  • 在內置鎖對象確認爲可偏向狀態之後,JVM檢查Mark Word中的線程ID是否爲搶鎖線程ID,如果是,就表示搶鎖線程處於偏向鎖狀態,搶鎖線程快速獲得鎖,開始執行臨界區代碼。
  • 如果Mark Word中的線程ID並未指向搶鎖線程,就通過CAS操作競爭鎖。如果競爭成功,就將Mark Word中的線程ID設置爲搶鎖線程,偏向標誌位設置爲1,鎖標誌位設置爲01,然後執行臨界區代碼,此時內置鎖對象處於偏向鎖狀態。
  • 如果CAS操作競爭失敗,就說明發生了競爭,撤銷偏向鎖,進而升級爲輕量級鎖
  • JVM使用CAS將鎖對象的Mark Word替換爲搶鎖線程的鎖記錄指針,如果成功,搶鎖線程就獲得鎖。如果替換失敗,就表示其他線程競爭鎖,JVM嘗試使用CAS自旋替換搶鎖線程的鎖記錄指針,如果自旋成功(搶鎖成功),那麼鎖對象依然處於輕量級鎖狀態。
  • 如果JVM的CAS替換鎖記錄指針自旋失敗,輕量級鎖就膨脹爲重量級鎖,後面等待鎖的線程也要進入阻塞狀態。

總體來說,偏向鎖是在沒有發生鎖爭用的情況下使用的;一旦有了第二個線程爭用鎖,偏向鎖就會升級爲輕量級鎖;如果鎖爭用很激烈,輕量級鎖的CAS自旋到達閾值後,輕量級鎖就會升級爲重量級鎖。

四.Java顯式鎖

在Java領域中,Java顯式鎖主要是指基於JDK層面實現的鎖。

v7MqVP.png

在Java領域中,基於JDK層面實現的鎖都存在於java.util.concurrent.locks包下面,大致可以分爲:

  • 基於Lock接口實現的鎖
  • 基於ReadWriteLock接口實現的鎖
  • 基於AQS基礎同步器實現的鎖
  • 基於自定義API操作實現的鎖

一直以來,併發編程領域,有兩大核心問題:一個是互斥,即同一時刻只允許一個線程訪問共享資源;另一個是同步,即線程之間如何通信、協作等。

Java SDK 併發包通過 Lock 和 Condition 兩個接口來實現管程,其中 Lock 用於解決互斥問題,Condition 用於解決同步問題。

1.JDK源碼

在Java領域中,Java顯式鎖從JDK源碼錶現出來的鎖大致可以分爲基於Lock接口實現的鎖,基於ReadWriteLock接口實現的鎖,基於AQS基礎同步器實現的鎖,以及基於自定義API操作實現的鎖等。

v7QFaV.png

在Java領域中,基於JDK源碼層面體現出來的鎖,主要分爲如下幾種:

  • 基於Lock接口實現的鎖:基於Lock接口實現的鎖主要有ReentrantLock。
  • 基於ReadWriteLock接口實現的鎖:基於ReadWriteLock接口實現的鎖主要有ReentrantReadWriteLock。
  • 基於AQS基礎同步器實現的鎖:基於AQS基礎同步器實現的鎖主要有CountDownLatch,Semaphore,ReentrantLock,ReentrantReadWriteLock等。
  • 基於自定義API操作實現的鎖: 不依賴於上述三種方式來直接封裝實現的鎖,最典型是JDK1.8版本中提供的StampedLock。

從一定程度上說,Java顯式鎖都是基於AQS基礎同步器實現的鎖,其中JDK1.8版本中提供的StampedLock是是對ReentrantReadWriteLock讀寫鎖的一種改進。

綜上所述,認識和掌握Java內置鎖,都需要AQS基礎同步器設計與實現,它是ava內置鎖的基礎和核心實現。

2.基本思想

在Java領域中,Java顯式鎖的基本思想來源於JDK併發包JUC的作者Doug Lea,發表的論文爲java.util.concurrent Synchronizer Framework 。

v7Q1IK.png

在Java領域中,同步器是指專門爲多線程併發而設計的同步機制,在這種機制下,多線程併發執行時線程之間通過某種共享狀態實現同步,只有滿足某種條件時線程才能執行。

在不同的應用場景中,對同步器的需求也不同,JDK將各種同步器的相同部分抽象封裝成一個統一的基礎同步器,然後基於這個同步器爲模板,通過繼承的方式來實現不同的同步器,即就是我們說的統一的基礎AQS同步器。

在JDK的併發包java.util.concurrent.下面,提供了各種同步工具,其中大部分同步工具都基於AbstractQueuedSynchronizer類實現,即就是AQS同步器,爲不同場景提供了實現鎖以及同步機制的基礎框架,爲同步狀態的原子性管理,線程阻塞與解除以及排隊管理提供一種通用的機制。

其中,AQS的理論基礎是JDK併發包JUC的作者Doug Lea,發表的論文爲java.util.concurrent Synchronizer Framework [AQS Framework論文],其中包括框架的基礎原理,需求,設計,實現思路,設計以及用戶和性能分析等。

3.基本實現

在Java領域中,Java顯式鎖從一定程度上說,Java顯式鎖都是基於AQS基礎同步器實現的鎖。

v7QYxH.png

從JDK1.8版本的源碼來看,AbstractQueuedSynchronizer的主要繼承了抽象類AbstractOwnableSynchronizer,其主要封裝了setExclusiveOwnerThread()和getExclusiveOwnerThread()兩個方法。其中:

  • setExclusiveOwnerThread()方法: 設置線程獨享模式,其參數爲java.lang.Thread對象。
  • getExclusiveOwnerThread()方法: 獲取獨享模式的線程,其返回參數類型爲java.lang.Thread對象。

對於一個AbstractQueuedSynchronizer(AQS同步器)從內部結構上來說,主要有5個核心要素: 同步狀態,等待隊列,獨佔模式,共享模式,條件隊列。其中:

v7QaqI.png

  • 同步狀態(Synchronizer Status):用於實現鎖機制
  • 等待隊列(Wait Queue):用於存儲等待鎖的線程
  • 獨佔模式(Exclusive Model): 實現獨佔鎖
  • 共享模式(Shared Model): 實現共享鎖
  • 條件隊列(Condition Queue):提供可替代wait/notify機制的條件隊列模式

從採用的數據結構來看,AQS同步器主要是將線程封裝到一個Node裏面,並維護一個CLH Node FIFO隊列(非阻塞FIFO隊列),以爲着在併發條件下,對此隊列中進行插入和移除操作時不會阻塞,主要是採用CAS+自旋鎖來保證節點的插入和移除的原子性操作,從而實現快速插入的。

從JDK1.8版本的源碼來看,AbstractQueuedSynchronizer的源碼結構主要如下:

  • 等待隊列:主要定義了兩個Node類變量,主要是等待隊列的結構變量head和tail等
  • 同步狀態爲state,其必須是32位整數類型,更新時必須保證是原子性的
  • CAS操作的變量:定義了stateOffset,headOffset,tailOffset,waitStatusOffset,nextOffset的句柄,主要用於執行CAS操作,其中JDK的CAS操作主要使用Unsafe類來實現,處於sun.misc.下面提供的類。
  • 閥值:spinForTimeoutThreshold,決定使用自旋方式消耗時間還是使用系統阻塞方式消耗時間的分割線,默認值爲1000L(ns),是長整數類型,表示鎖競爭小於1000(ns)使用自旋,如果超過1000(ns)使用系統阻塞。
  • 條件隊列對象:基於Condition接口封裝了一個ConditionObject對象。

但是,特別需要注意的是,在JDK1.8版本之後,AbstractQueuedSynchronizer的源碼結構有所不同:

  • 等待隊列:主要定義了兩個Node類變量,主要是等待隊列的結構變量head和tail
  • 同步狀態爲state,其必須是32位整數類型,更新時必須保證是原子性的
  • CAS操作的變量:使用VarHandle定義了state,head,tail的句柄,主要用於執行CAS操作,其中JDK1.9的CAS操作主要使用VarHandle來替代Unsafe類,位於java.lang.invoke.下面。
  • 閥值:spinForTimeoutThreshold,決定使用自旋方式消耗時間還是使用系統阻塞方式消耗時間的分割線,默認值爲1000L(ns),是長整數類型,表示鎖競爭小於1000(ns)使用自旋,如果超過1000(ns)使用系統阻塞。
  • 條件隊列對象:基於Condition接口封裝了一個ConditionObject對象。

由此可見,最大的不同就是使用VarHandle來替代Unsafe類,Varhandle是對變量或參數定義的變量系列的動態強類型引用,包括靜態字段,非靜態字段,數組元素或堆外數據結構的組件。 在各種訪問模式下都支持訪問這些變量,包括簡單的讀/寫訪問,volatile 的讀/寫訪問以及 CAS (compare-and-set)訪問。簡單來說 Variable 就是對這些變量進行綁定,通過 Varhandle 直接對這些變量進行操。

4.具體實現

在Java領域中,Java顯式鎖中基於AQS基礎同步器實現的鎖主要都是採用自旋鎖(CLH鎖)+CAS操作來實現。

v7QNMd.png

在介紹內置鎖的時候,提到輕量級鎖的主要分類爲普通自旋鎖和自適應自旋鎖,但其實對於自旋鎖的實現方式來看,主要可以分爲普通自旋鎖和自適應自旋鎖,CLH鎖和MCS鎖等4種,其中:

  • 普通自旋鎖:多個線程不斷自旋,不斷嘗試獲取鎖,其不具備公平性和由於要保證CPU和緩存以及主存之間的數據一致性,其開銷較大。
  • 自適應自旋鎖:主要是爲解決普通自旋鎖的公平性問題,引入了一個排隊機制,一般稱爲排他自旋鎖,其具備公平性,但是沒有解決保證CPU和緩存以及主存之間的數據一致性問題,其開銷較大。
  • CLH鎖:通過一定手段將線程對於某一個共享變量的輪詢競爭轉化爲一個線程隊列,且隊列中的線程各自輪詢自己本地變量。
  • MCS鎖:主旨在於解決 CLH鎖的問題,也是基於FIFO隊列,與CLH鎖不同是,只對本地變量自旋,前驅節點負責通知MCS鎖中線程自適結束。

自旋鎖是一種實現同步的方案,屬於一種非阻塞鎖,與常規鎖主要的區別就在於獲取鎖失敗之後的處理方式不同,主要體現在:

  • 一般情況下,常規鎖在獲取鎖失敗之後,會將線程阻塞並適當時重新喚醒
  • 而自旋鎖則是使用自旋來替換阻塞操作,主要是線程會不斷循環檢查該鎖是否被釋放,一旦釋放線程便會獲取鎖資源。

其實,自旋是一鍾忙等待狀態,會一直消耗CPU的執行時間。一般情況下,常規互斥鎖適用於持有鎖長時間的情況,自旋鎖適合持有時間短的情況。

其中,對於CLH鎖來說,其核心是爲解決同步帶來的花銷問題,Craig,Landim,Hagersten三人發明了CLH鎖,其中主要是:

  • 構建一個FIFO(先進先出)隊列,構建時主要通過移動尾部節點tail來實現隊列的排隊,每個想獲得鎖的線程都會創建一個新節點(next)並通過CAS操作原子操作將新節點賦予給tail,當前線程輪詢前一個節點的狀態。
  • 執行完線程後,只需將當前線程對應節點狀態設置爲解鎖即可,主要是判斷當前節點是否爲尾部節點,如果是直接設置尾部節點設置爲空。由於下一個節點一直在輪詢,所以可以獲得鎖。

CLH鎖將衆多線程長時間對資源的競爭,通過有序化這些線程將其轉化爲只需要對本地變量檢測。唯一存在競爭的地方就是入隊之前對尾部節點tail 的競爭,相對來說,當前線程對資源的競爭次數減少,這節省了CPU緩存同步的消耗,從而提升了系統性能。

但是同時也有一個問題,CLH鎖雖然解決了大量線程同時操作同一個變量時帶來的開銷問題,如果前驅節點和當前節點在本地主存中不存在,則訪問時間過長,也會引起性能問題。MCS鎖就時爲解決這個問題提出的,作者主要是John Mellor Curmmey和Michhael Scott兩人發明的。

而對於CAS操作來說,CAS(Compare And Swap,比較並交換)操作時一種樂觀鎖策略,主要涉及三個操作數據:內存值,預期值,新值,主要是指當且僅當預期值和內存值相等時纔去修改內存值爲新值。

CAS操作的具體邏輯,主要可以分爲三個步驟:

  • 首先,檢查某個內存值是否與該線程之前取到值一樣。
  • 其次,如果不一樣,表示此內存值已經被別的線程修改,需要捨棄本次操作。
  • 最後,如果時一樣,表示期間沒有線程更改過,則需要用新值執行更新內存值。

除此之外,需要注意的是CAS操作具有原子性,主要是由CPU硬件指令來保證,並且通過Java本地接口(Java Native Interface,JNI)調用本地硬件指令實現。

當然,CAS操作避免了悲觀策略獨佔對象的 問題,同時提高了併發性能,但是也有以下三個問題:

  • 樂觀策略只能保證一個共享變量的原子操作,如果是多個變量,CAS便不如互斥鎖,主要是CAS操作的侷限所致。
  • 長時間循環操作可能導致開銷過大。
  • 經典的ABA問題: 主要是檢查某個內存值是否與該線程之前取到值一樣,這個判斷邏輯不嚴謹。解決ABA問題的核心在於,引入版本號,每次更新變量值更新版本號。

其中,在Java領域中,對於CAS操作在

  • JDK1.8版本之前,CAS操作主要使用Unsafe類,具體可以參考源碼自行分析。
  • JDK1.8版本之後,JDK1.9的CAS操作主要使用VarHandle類,具體可以參考源碼自行分析。

綜上所述,主要說明Java顯式鎖爲啥使用基於AQS基礎同步器實現的鎖主要都是採用自旋鎖(CLH鎖)+CAS操作來的具體實現。

5.基本分類

在Java領域中,Java顯式鎖的基本分類大致可以分爲可重入鎖和不可重入鎖、悲觀鎖和樂觀鎖、公平鎖和非公平鎖、共享鎖和獨佔鎖、可中斷鎖和不可中斷鎖。

v7lAeI.png

顯式鎖有很多種,從不同的角度來看,顯式鎖大概有以下幾種分類:可重入鎖和不可重入鎖、悲觀鎖和樂觀鎖、公平鎖和非公平鎖、共享鎖和獨佔鎖、可中斷鎖和不可中斷鎖。

從同一個線程是否可以重複佔有同一個鎖對象的角度來分,顯式鎖可以分爲可重入鎖與不可重入鎖。其中:

  • 可重入鎖也叫作遞歸鎖,指的是一個線程可以多次搶佔同一個鎖,JUC的ReentrantLock類是可重入鎖的一個標準實現類。
  • 不可重入鎖與可重入鎖相反,指的是一個線程只能搶佔一次同一個鎖。

從線程進入臨界區前是否鎖住同步資源的角度來分,顯式鎖可以分爲悲觀鎖和樂觀鎖。其中:

  • 悲觀鎖:就是悲觀思想,每次進入臨界區操作數據的時候都認爲別的線程會修改,所以線程每次在讀寫數據時都會上鎖,鎖住同步資源,這樣其他線程需要讀寫這個數據時就會阻塞,一直等到拿到鎖。總體來說,悲觀鎖適用於寫多讀少的場景,遇到高併發寫時性能高。Java的synchronized重量級鎖是一種悲觀鎖。
  • 樂觀鎖是一種樂觀思想,每次去拿數據的時候都認爲別的線程不會修改,所以不會上鎖,但是在更新的時候會判斷一下在此期間別人有沒有去更新這個數據,採取在寫時先讀出當前版本號,然後加鎖操作(比較跟上一次的版本號,如果一樣就更新),如果失敗就要重複讀-比較-寫的操作。總體來說,樂觀鎖適用於讀多寫少的場景,遇到高併發寫時性能低。Java中的樂觀鎖基本都是通過CAS自旋操作實現的。CAS是一種更新原子操作,比較當前值跟傳入值是否一樣,是則更新,不是則失敗。在爭用激烈的場景下,CAS自旋會出現大量的空自旋,會導致樂觀鎖性能大大降低。Java的synchronized輕量級鎖是一種樂觀鎖。另外,JUC中基於抽
    象隊列同步器(AQS)實現的顯式鎖(如ReentrantLock)都是樂觀鎖。

從搶佔資源的公平性來說,顯示鎖可以分爲公平鎖和非公平鎖,其中:

  • 公平鎖是指不同的線程搶佔鎖的機會是公平的、平等的,從搶佔時間上來說,先對鎖進行搶佔的線程一定被先滿足,搶鎖成功的次序體現爲FIFO(先進先出)順序。簡單來說,公平鎖就是保障各個線程獲取鎖都是按照順序來的,先到的線程先獲取鎖。
  • 非公平鎖是指不同的線程搶佔鎖的機會是非公平的、不平等的,從搶佔時間上來說,先對鎖進行搶佔的線程不一定被先滿足,搶鎖成功的次序不會體現爲FIFO(先進先出)順序。

默認情況下,ReentrantLock實例是非公平鎖,但是,如果在實例構造時傳入了參數true,所得到的鎖就是公平鎖。另外,ReentrantLock的tryLock()方法是一個特例,一旦有線程釋放了鎖,正在tryLock的線程就能優先取到鎖,即使已經有其他線程在等待隊列中。

從在搶鎖過程中能通過某些方法終止搶佔過程角度來看,顯式鎖可以分爲可中斷鎖和不可中斷鎖,其中:

  • 可中斷鎖:什麼是可中斷鎖?如果某一線程A正佔有鎖在執行臨界區代碼,另一線程B正在阻塞式搶佔鎖,可能由於等待時間過長,線程B不想等待了,想先處理其他事情,我們可以讓它中斷自己的阻塞等待,
  • 不可中斷鎖: 什麼是不可中斷鎖?一旦這個鎖被其他線程佔有,如果自己還想搶佔,只能選擇等待或者阻塞,直到別的線程釋放這個鎖,如果別的線程永遠不釋放鎖,那麼自己只能永遠等下去,並且沒有辦法終止等
    待或阻塞。

簡單來說,在搶鎖過程中能通過某些方法終止搶佔過程,這就是可中斷鎖,否則就是不可中斷鎖。

Java的synchronized內置鎖就是一個不可中斷鎖,而JUC的顯式鎖(如ReentrantLock)是一個可中斷鎖。

  • 獨佔鎖指的是每次只有一個線程能持有的鎖。獨佔鎖是一種悲觀保守的加鎖策略,它不必要地限制了讀/讀競爭,如果某個只讀線程獲取鎖,那麼其他的讀線程都只能等待,這種情況下就限制了讀操作的
    併發性,因爲讀操作並不會影響數據的一致性。JUC的ReentrantLock類是一個標準的獨佔鎖實現類。

  • 共享鎖允許多個線程同時獲取鎖,容許線程併發進入臨界區。與獨佔鎖不同,共享鎖是一種樂觀鎖,它放寬了加鎖策略,並不限制讀/讀競爭,允許多個執行讀操作的線程同時訪問共享資源。JUC的ReentrantReadWriteLock(讀寫鎖)類是一個共享鎖實現類。使用該讀寫鎖時,讀操作可以有很多線程一起讀,但是寫操作只能有一個線程去寫,而且在寫入的時候,別的線程也不能進行讀的操作。用ReentrantLock鎖替代ReentrantReadWriteLock鎖雖然可以保證線程安全,但是也會浪費一部分資源,因爲多個讀操作並沒有線程安全問題,所以在讀的地方使用讀鎖,在寫的地方使用寫鎖,可以提高程序執行效率。

綜上所述,對於Java顯式鎖的基本分類,一般情況下我們都可按照這樣的方式去分析。

6.應用分析

在Java領域中,Java顯式鎖的Java顯式鎖比Java內置鎖的鎖粒度更細膩,可以設置超時機制,更加可控,使用起來更加靈活。

v7llOs.png

對比基於Java內置鎖實現一種簡單的“等待-通知”方式的線程間通信:通過Object對象的wait、notify兩類方法作爲開關信號,用來完成通知方線程和等待方線程之間的通信。

“等待-通知”方式的線程間通信機制,具體來說是指一個線程A調用了同步對象的wait()方法進入等待狀態,而另一線程B調用了同步對象的notify()或者notifyAll()方法去喚醒等待線程,當線程A收到線程B的喚醒通知後,就可以重新開始執行了。

需要特別注意的是,在通信過程中,線程需要擁有同步對象的監視器,在執行Object對象的wait、notify方法之前,線程必須先通過搶佔到內置鎖而成爲其監視器的Owner。

與Object對象的wait、notify兩類方法相類似,JUC也爲大家提供了一個用於線程間進行“等待-通知”方式通信的接口——java.util.concurrent.locks.Condition。其中:

v7l3mn.png

  • await()方法:喚醒一個等待隊列
  • awaitUninterruptibly() 方法:喚醒一個不可中斷的等待隊列
  • awaitNanos(long nanosTimeout) 方法:喚醒一個帶超時的等待隊列
  • await(long time, TimeUnit unit)方法:喚醒一個帶超時的等待隊列
  • awaitUntil(Date deadline) 方法:喚醒一個帶超時的等待隊列
  • signal()方法:隨機地通知等待隊列中的一個線程
  • signalAll()方法:通知等待隊列中的所有線程

同時,JUC提供的一個線程阻塞與喚醒的工具類(java.util.concurrent.locks.LockSupport),該工具類可以讓線程在任意位置阻塞和喚醒,其所有的方法都是靜態方法。

v7lapF.png

  • void park()方法: 對當前線程執行阻塞操作,直到獲取許可後才解除阻塞
  • void parkNanos(long nanos)方法:對當前線程執行阻塞操作,直到獲取許可後才解除阻塞,最大等待時間有參數傳入指定,一旦超過最大時間也會解除阻塞
  • void parkNanos(Object blocker, long nanos)方法:對當前線程執行阻塞操作,直到獲取許可後才解除阻塞,最大等待時間有參數傳入指定,一旦超過最大時間也會解除阻塞,需要指定阻塞對象
  • void parkUntil(long deadline)方法:對當前線程執行阻塞操作,直到獲取許可後才解除阻塞最大等待時間爲指定最後期限
  • void parkUntil(Object blocker, long deadline)方法: 對當前線程執行阻塞操作,直到獲取許可後才解除阻塞最大等待時間爲指定最後期限,需要指定阻塞對象
  • void unpark(Thread thread)方法: 將指定線程設置爲可用

相比之下,Java顯式鎖比Java內置鎖的鎖粒度更細膩,可以設置超時機制,更加可控,使用起來更加靈活。

五.Java鎖綜合對比分析

Java鎖綜合對比分析主要是對Java內置鎖和Java顯式鎖等作一個對比分析,看看兩者之間各自的特點。

v7G2VA.png

在Java領域中,對於Java內置鎖和Java顯式鎖,一般可以從以下幾個方面去看:

  • 從基本定義上來看,Java內置鎖是基於Java語法層面實現的鎖,而Java顯式鎖是基於JDK層面實現的鎖
  • 從基本思想上來看,Java內置鎖是關鍵字+Object類中wait()、notify()、notifyAll() 方法來實現“等待-通知“工作機制,而Java顯式鎖是基於統一的AQS基礎同步器+條件隊列Condition對象+LockSupport線程阻塞與喚醒的工具類來實現“等待-通知“工作機制的,兩者之間都可以用於實現線程之間的 通信
  • 從實現方式上來看,Java內置鎖是通過JVM中通過Monitor來實現monitorenter和monitorexit指令實現,底層是調用操作系統的互斥鎖原語實現,而Java顯式鎖是基於統一的AQS基礎同步器來實現的
  • 從底層結構上來看,Java內置鎖是基於JVM中Monitor與ObjectMonitor映射對應+CAS操作來實現的,而Java顯式鎖是基於CLH鎖 Node FIFO 隊列(先進先出)隊列+CAS操作來實現
  • 從鎖粒度細分上來看,Java內置鎖是鎖粒度比較大,相對比較粗,而Java顯式鎖的鎖粒度比較小,相對比較細膩
  • 從鎖是否支持超時中斷來看,Java內置鎖是不支持超時,不可中斷,發生異常自動釋放鎖或阻塞,而Java顯式鎖是支持超時,可中斷,發生異常自動釋放鎖或自旋
  • 從使用方式上看,Java內置鎖是使用簡單,可編程性較低,而Java顯式鎖是使用方式比較靈活,可編程性較高
  • 從鎖資源和目標上看,Java內置鎖是面向是類和對象中方法以及變量,而Java顯式鎖是面向的是線程本身和線程狀態的控制
  • 從鎖的公平性保證上來看,Java內置鎖是無法保證鎖的公平性,而Java顯式鎖是可以實現和保障鎖的公平性的
  • 從併發三宗罪來看,Java內置鎖是可以解決併發問題的原子性和可見性,而對於有序性問題是交給編譯器來實現,而Java顯式鎖可以解決併發問題的原子性和可見性以及有序性問題
  • 從線程飢餓問題來看,Java內置鎖是可能產生線程飢餓問題,而Java顯式鎖是可以防止和解決線程飢餓問題的
  • 從線程競爭問題來看,Java內置鎖是可能產生線程競爭問題,而Java顯式鎖是可以防止和解決線程競爭問題的
  • 從線程競爭條件問題來看,Java內置鎖是可能產生線程競爭條件問題,而Java顯式鎖是可以防止和解決線程競爭條件問題的

綜上所述,通過對Java鎖綜合對比分析,我相信大家對於Java領域中的鎖已經可以很好地認識以及深入瞭解。

寫在最後

Picture-Footer

對於Java 領域中鎖,我們一般可以從如下兩個方面去認識,其中:

  • Java內置鎖:基於Java語法層面(關鍵詞)實現的鎖,主要是根據Java語義來實現,最典型的應用就是synchronized。
  • Java顯式鎖:基於JDK層面實現的鎖,主要是根據基於Lock接口和ReadWriteLock接口,以及統一的AQS基礎同步器等來實現,最典型的有ReentrantLock。

對於Java內置鎖來說:

  • 使用方式:synchronized關鍵字互斥鎖主要有作用於對象方法上面,作用於類靜態方法上面,作用於對象方法裏面,作用於類靜態方法裏面等4種方式。
  • 基本思想:synchronized關鍵字互斥鎖主要基於一個阻塞隊列和等待對列,類似於一種“等待-通知”的工作機制來實現。
  • 基本實現:synchronized關鍵字互斥鎖主要基於Java HotSpot(TM) VM 虛擬機通過Monitor(監視器)來實現monitorenter和monitorexit指令的。
  • 具體實現:JVM中每個對象都會有一個監視器,監視器和對象一起創建、銷燬。監視器相當於一個用來監視這些線程進入的特殊房間,其義務是保證(同一時間)只有一個線程可以訪問被保護的臨界區代碼塊。
  • 基本分類: synchronized關鍵字互斥鎖主要中內置鎖一共有4種狀態:無鎖狀態、偏向鎖狀態、輕量級鎖狀態和重量級鎖狀態,這些狀態隨着競爭情況逐漸升級,其中升級順序爲:無鎖->偏向鎖->輕量級鎖狀態->重量級,其順序不可逆轉。
  • 應用分析: synchronized關鍵字互斥鎖主要中內置鎖使用簡單,但是鎖的粒度比較大,無法支持超時等。

對於Java顯式鎖來說:

  • 使用方式:Java顯式鎖從JDK源碼錶現出來的鎖大致可以分爲基於Lock接口實現的鎖,基於ReadWriteLock接口實現的鎖,基於AQS基礎同步器實現的鎖,以及基於自定義API操作實現的鎖等。
  • 基本思想:Java顯式鎖的基本思想來源於JDK併發包JUC的作者Doug Lea,發表的論文爲java.util.concurrent Synchronizer Framework 。
  • 基本實現:Java顯式鎖從一定程度上說,Java顯式鎖都是基於AQS基礎同步器實現的鎖。
  • 具體實現:Java顯式鎖中基於AQS基礎同步器實現的鎖主要都是採用自旋鎖(CLH鎖)+CAS操作來實現。
  • 基本分類:Java顯式鎖的基本分類大致可以分爲可重入鎖和不可重入鎖、悲觀鎖和樂觀鎖、公平鎖和非公平鎖、共享鎖和獨佔鎖、可中斷鎖和不可中斷鎖。
  • 應用分析: Java顯式鎖的Java顯式鎖比Java內置鎖的鎖粒度更細膩,可以設置超時機制,更加可控,使用起來更加靈活。

最後,技術研究之路任重而道遠,願我們熬的每一個通宵,都撐得起我們想在這條路上走下去的勇氣,未來仍然可期,與君共勉!

版權聲明:本文爲博主原創文章,遵循相關版權協議,如若轉載或者分享請附上原文出處鏈接和鏈接來源。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章