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

寫在開頭

對于Java領域中的鎖,其實從接觸Java至今,我相信每一位Java Developer都會有這樣的一個感覺?不論是Java對鎖的實作還是應用,真的是一種“群英薈萃”,而且每一種鎖都有點各有各的驢,各有各的本,各不相同,
在很多情況下,以及在各種鎖的應用場景里,各式各樣的定義,難免會讓我們覺得無所適從,很難清楚該如何對這些鎖做到得心應手?
在并發編程色世界中,一般情況下,我們只需了解其是如何使用鎖之后就已經滿足我們大部分的需求,但是作為一名對技術研究有執念和熱情的人來說,深入探究和分析才是對技術的探秘之樂趣,
作為一名Java Developer來說,深入探究和分析和正確了解和掌握這些鎖的機制和原理,需要我們帶著一些實際問題,通過對其探究分析和加上實際應用分析,才能真正意義上理解和掌握,
一般來說,針對于不同場景提供的鎖,都用于解決什么問題?不論是從實作方式上,還是從使用場景上,都可以應對這些鎖的特點,我們又該如何認識和理解?
接下來,今天我們就一起來盤一盤,Java領域中那些并發鎖,盤點一下相關的鎖,從設計基本思想和設計實作,以及應用分析等方面來總體分析探討一下,
關健術語

本文用到的一些關鍵詞語以及常用術語,主要如下:
- 行程(Process): 計算機中的程式關于某資料集合上的一次運行活動,是系統進行資源分配和調度的基本單位,是作業系統結構的基礎,在早期面向行程設計的計算機結構中,行程是程式的基本執行物體;在當代面向執行緒設計的計算機結構中,行程是執行緒的容器,
- 執行緒(thread): 作業系統能夠進行運算調度的最小單位,它被包含在行程之中,是行程中的實際運作單位,在Unix System V及SunOS中也被稱為輕量行程(Light-Weight Processes),但輕量行程更多指內核執行緒(Kernel Thread),而把用戶執行緒(User Thread)稱為執行緒,
基本概述

在Java領域中,單純從Java對其實作的方式上來看,我們大體上可以將其分為基于Java語法層面(關鍵詞)實作的鎖和基于JDK層面實作的鎖,
基于這兩個基本點,可以作為我們對于Java領域中的鎖的一個基礎認識,這對于我們認識和了解Java領域中的鎖指導一個參考方向,
一般來說,鎖是并發編程中最基礎和最常用的一項技術,而且在Java的內部JDK中其使用也是非常地廣泛,
接下來,我們便一起探究和認識一下Java領域中的各種各樣的鎖,
一.鎖的基本理論
鎖的基本理論主要是指從鎖的基本定義和基本特點以及基本意義去分析的一般模型理論,是一套幫助我們認識和了解鎖的簡單的思維方法論,

一般在了解一個事物之前,我們都會按斬訓本定義,基本特點以及基本意義去看待這個事物,在計算機的世界里,鎖本身也和我們實際生活一樣,也是一個比較普遍且應用場景繁多的一種事物,
比如,在作業系統中,也定義了各種各樣的鎖;在資料庫系統中也出現了鎖,甚至,在CPU處理器架構中都會看見鎖的身影,
但是,這里就會有一個問題:既然都在使用鎖,可是對于鎖該去如何定義,似乎都很難給出一個準確的定義? 換而言之,這也許就是我們對于鎖只是知道有這個東西,但是一直有云里霧里的基本原因,
從本質上講,計算機軟體開發領域中的鎖是一種協調多個行程 或者多個執行緒對某一個資源的訪問的控制機制,其核心是作用于資源,也作用于著這個定義中提到的行程和執行緒等,其中:

- 行程(Process): 作業系統進行資源分配和調度的基本單位,是計算機程式中的物體,其中,程式是指令、資料及其組織形式的描述,
- 執行緒(Thread) : 作業系統能夠進行運算調度的最小單位,一條執行緒指的是行程中一個單一順序的控制流,一個行程中可以并發多個執行緒,每條執行緒并行執行不同的任務,
一般來說,執行緒主要分為位于系統內核空間的執行緒稱為內核執行緒(Kernel Thread)和位于應用程式的用戶空間的執行緒被稱為用戶執行緒(User Thread)兩種,其中:

也就是我們一般說的Java執行緒等均屬于用戶執行緒,而內核執行緒主要是作業系統封裝的函式庫以及API等,
而且最關健的就是,我們平日里所提到Java執行緒和JVM都是位于用戶空間之中,從Java層到作業系統系統的執行緒調度順序來看,一般流程是:java.lang.Thread(Target Thread)->Java Thread->OSThread->pthread->Kernel Thread,

簡單來說,在Java領域中,鎖是用于控制多個執行緒訪問共享資源的工具,一般,鎖提供對共享資源的獨立訪問:一次只有一個執行緒可以獲取鎖,所有對共享資源的訪問都需要先獲取鎖,但是,某些鎖可以并發訪問共享資源,
對于并發訪問共享資源來說,主要是依據現在大多數作業系統的執行緒的調度方式是搶占式調度,因此加鎖是為了維護資料的一致性和完整性,其實就是資料的安全性,
綜上所述,我們便可以得到一個關于鎖的基本概念模型,接下來我們便來一一盤點以下主要有哪些鎖,
二.鎖的基本分類
在Java領域中,我們可以將鎖大致分為基于Java語法層面(關鍵詞)實作的鎖和基于JDK層面實作的鎖,

單純從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語法層面(關鍵詞)實作的鎖,

在Java領域中,我們把基于Java語法層面(關鍵詞)實作的鎖稱為內置鎖,比如synchronized 關鍵字,
對于synchronized 關鍵字的解釋,最直接的就是Java語言中為開發人員提供的同步工具,可以看作是Java中的一種“語法糖”,主要宗旨在于解決多執行緒并發執行程序中資料同步的問題,
不像其他的編程語言(C++),在處理同步問題時都需要自己進行鎖處理,主要特點就是簡單,直接宣告即可,
在 Java 程式中,利用 synchronized 關鍵字來對程式進行加鎖,其實作同步的語意是互斥鎖,既可以用來宣告一個 synchronized 代碼塊,也可以直接標記靜態方法或者實體方法,
其中,對于互斥的概念來說,在數學范疇來講,是一個數學名詞,表示和描述的是事件A與事件B在任何一次試驗中都不會同時發生,則稱事件A與事件B互斥,
因此,對于互斥鎖可以理解為: 對于某一個鎖來說,任意時刻只能有一個執行緒獲得該鎖,對于其他執行緒想獲取鎖的時候就得等待或者被阻塞,
1.使用方式
在Java領域中,synchronized關鍵字互斥鎖主要有作用于物件方法上面,作用于類靜態方法上面,作用于物件方法里面,作用于類靜態方法里面等4種方式,

在Java領域中,synchronized關鍵字從使用方式來看,主要可以分為:
- 作用于物件方法上面:
- 描述物件的方法,表示該物件的方法具有同步性,由于描述的物件的方法,作用范圍是在物件(Object),整個物件充當了鎖,
- 需要注意的是,類可以實體化多個物件,這時每一個物件都是一個鎖,每個鎖的范圍相當于是當前物件來說的,
- 作用于類靜態方法上面:
- 描述類的靜態方法,表示該方法具有同步性,由于描述的類靜態的方法,作用范圍是在類(Class),整個類充當了鎖,
- 需要注意的是,某一個類的本身也是一個物件,JVM使用這個物件作為模板去生成該類的物件時,每個鎖的范圍相當于是當前類來說的,
- 作用于物件方法里面:
- 描述方法內部的某塊邏輯,表示該代碼塊具有同步性,
- 需要注意的是,一般需要我們指定物件,比如synchronized(this){xxx}是指當前物件的,也可以創建一個物件來作為鎖,
- 作用于類靜態方法里面:
- 描述靜態方法內部的某塊邏輯,表示該代碼塊具有同步性,
- 需要注意的是,一般需要我們指定鎖物件,比如synchronized(this){xxx}是指當前類class作為鎖物件的,也可以創建一個物件來作為鎖,
一般當我們在撰寫代碼的程序中,如果按照上述方式宣告時,被synchronized關鍵字宣告的代碼會比普通代碼在編譯之后,使用javap -c xxx.class 查看位元組碼,就會發現多兩個monitorenter和monitorexit指令,
2.基本思想
在Java領域中,synchronized關鍵字互斥鎖主要基于一個阻塞佇列和等待對列,類似于一種“等待-通知”的作業機制來實作,

一般情況下,“等待 - 通知”的作業機制的要求是執行緒首先獲取互斥鎖,其中:
- 當執行緒要求的條件不滿足時,釋放互斥鎖,進入等待狀態,
- 當要求的條件滿足時,通知等待的執行緒,重新獲取互斥鎖,
在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 保護的臨界區,當有一個執行緒進入臨界區后,其他執行緒就只能進入圖中左邊的等待佇列里等待,這個等待佇列和互斥鎖是一對一的關系,每個互斥鎖都有自己獨立的等待佇列,在并發程式中,其中:

- 當一個執行緒進入臨界區后,由于某些條件不滿足,需要進入等待狀態,Java 物件的 wait() 方法就能夠滿足這種需求,
- 當呼叫 wait() 方法后,當前執行緒就會被阻塞,并且進入到右邊的等待佇列中,這個等待佇列也是互斥鎖的等待佇列,
- 執行緒在進入等待佇列的同時,會釋放持有的互斥鎖,執行緒釋放鎖后,其他執行緒就有機會獲得鎖,并進入臨界區了,
對于通知佇列的作業機制來說,那執行緒要求的條件滿足時,該怎么通知這個等待的執行緒呢?很簡單,就是 Java 物件的 notify() 和 notifyAll() 方法,當條件滿足時呼叫 notify(),會通知等待佇列(互斥鎖的等待佇列)中的執行緒,告訴它條件曾經滿足過,為什么說是曾經滿足過呢?其中:

- 因為 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指令的,

在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中每個物件都會有一個監視器,監視器和物件一起創建、銷毀,監視器相當于一個用來監視這些執行緒進入的特殊房間,其義務是保證(同一時間)只有一個執行緒可以訪問被保護的臨界區代碼塊,

本質上,監視器是一種同步工具,也可以說是一種同步機制,主要特點是:
- 同步:監視器所保護的臨界區代碼是互斥地執行的,一個監視器是一個運行許可,任一執行緒進入臨界區代碼都需要獲得這個許可,離開時把許可歸還,
- 協作:監視器提供Signal機制,允許正持有許可的執行緒暫時放棄許可進入阻塞等待狀態,等待其他執行緒發送Signal去喚醒;其他擁有許可的執行緒可以發送Signal,喚醒正在阻塞等待的執行緒,讓它可以重新獲得許可并啟動執行,
在Hotspot虛擬機中,監視器是由C++類ObjectMonitor實作的,ObjectMonitor類定義在ObjectMonitor.hpp檔案中,ObjectMonitor的Owner(_owner)、WaitSet(_WaitSet)、Cxq(_cxq)、EntryList(_EntryList)這幾個屬性比較關鍵,

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種狀態:無鎖狀態、偏向鎖狀態、輕量級鎖狀態和重量級鎖狀態,這些狀態隨著競爭情況逐漸升級,

在Java領域中,一般Java物件(Object實體)結構包括三部分:物件頭、物件體和對齊位元組,其中:

- 物件頭(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內核態下的互斥鎖,升級為重量級鎖時,等待鎖的執行緒都會進入阻塞狀態,其開銷較大,
從鎖升級的狀態順序來看,只能是: 無鎖->偏向鎖->輕量級鎖->重量級鎖 ,而且順序不可逆,也就是不能降級,

綜上所述,在Java內置鎖中,偏向鎖通過對比Mark Word解決加鎖問題,避免執行CAS操作,而輕量級鎖是通過用CAS操作和自旋來解決加鎖問題,避免執行緒阻塞和喚醒而影響性能,重量級鎖是將除了擁有鎖的執行緒以外的執行緒都阻塞,
6.應用分析
在Java領域中,synchronized關鍵字互斥鎖主要中內置鎖使用簡單,但是鎖的粒度比較大,無法支持超時等,

從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層面實作的鎖,

在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操作實作的鎖等,

在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 ,

在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基礎同步器實作的鎖,

從JDK1.8版本的原始碼來看,AbstractQueuedSynchronizer的主要繼承了抽象類AbstractOwnableSynchronizer,其主要封裝了setExclusiveOwnerThread()和getExclusiveOwnerThread()兩個方法,其中:
- setExclusiveOwnerThread()方法: 設定執行緒獨享模式,其引數為java.lang.Thread物件,
- getExclusiveOwnerThread()方法: 獲取獨享模式的執行緒,其回傳引數型別為java.lang.Thread物件,
對于一個AbstractQueuedSynchronizer(AQS同步器)從內部結構上來說,主要有5個核心要素: 同步狀態,等待佇列,獨占模式,共享模式,條件佇列,其中:

- 同步狀態(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操作來實作,

在介紹內置鎖的時候,提到輕量級鎖的主要分類為普通自旋鎖和自適應自旋鎖,但其實對于自旋鎖的實作方式來看,主要可以分為普通自旋鎖和自適應自旋鎖,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顯式鎖的基本分類大致可以分為可重入鎖和不可重入鎖、悲觀鎖和樂觀鎖、公平鎖和非公平鎖、共享鎖和獨占鎖、可中斷鎖和不可中斷鎖,

顯式鎖有很多種,從不同的角度來看,顯式鎖大概有以下幾種分類:可重入鎖和不可重入鎖、悲觀鎖和樂觀鎖、公平鎖和非公平鎖、共享鎖和獨占鎖、可中斷鎖和不可中斷鎖,
從同一個執行緒是否可以重復占有同一個鎖物件的角度來分,顯式鎖可以分為可重入鎖與不可重入鎖,其中:
- 可重入鎖也叫作遞回鎖,指的是一個執行緒可以多次搶占同一個鎖,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內置鎖的鎖粒度更細膩,可以設定超時機制,更加可控,使用起來更加靈活,

對比基于Java內置鎖實作一種簡單的“等待-通知”方式的執行緒間通信:通過Object物件的wait、notify兩類方法作為開關信號,用來完成通知方執行緒和等待方執行緒之間的通信,
“等待-通知”方式的執行緒間通信機制,具體來說是指一個執行緒A呼叫了同步物件的wait()方法進入等待狀態,而另一執行緒B呼叫了同步物件的notify()或者notifyAll()方法去喚醒等待執行緒,當執行緒A收到執行緒B的喚醒通知后,就可以重新開始執行了,
需要特別注意的是,在通信程序中,執行緒需要擁有同步物件的監視器,在執行Object物件的wait、notify方法之前,執行緒必須先通過搶占到內置鎖而成為其監視器的Owner,
與Object物件的wait、notify兩類方法相類似,JUC也為大家提供了一個用于執行緒間進行“等待-通知”方式通信的介面——java.util.concurrent.locks.Condition,其中:

- await()方法:喚醒一個等待佇列
- awaitUninterruptibly() 方法:喚醒一個不可中斷的等待佇列
- awaitNanos(long nanosTimeout) 方法:喚醒一個帶超時的等待佇列
- await(long time, TimeUnit unit)方法:喚醒一個帶超時的等待佇列
- awaitUntil(Date deadline) 方法:喚醒一個帶超時的等待佇列
- signal()方法:隨機地通知等待佇列中的一個執行緒
- signalAll()方法:通知等待佇列中的所有執行緒
同時,JUC提供的一個執行緒阻塞與喚醒的工具類(java.util.concurrent.locks.LockSupport),該工具類可以讓執行緒在任意位置阻塞和喚醒,其所有的方法都是靜態方法,

- 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顯式鎖等作一個對比分析,看看兩者之間各自的特點,

在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領域中的鎖已經可以很好地認識以及深入了解,
寫在最后

對于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內置鎖的鎖粒度更細膩,可以設定超時機制,更加可控,使用起來更加靈活,
最后,技術研究之路任重而道遠,愿我們熬的每一個通宵,都撐得起我們想在這條路上走下去的勇氣,未來仍然可期,與君共勉!
著作權宣告:本文為博主原創文章,遵循相關著作權協議,如若轉載或者分享請附上原文出處鏈接和鏈接來源,
轉載請註明出處,本文鏈接:https://www.uj5u.com/ruanti/505352.html
標籤:架構設計
