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

寫在開頭

提起Java領域中的鎖,是否有種“道不盡紅塵奢戀,訴不完人間恩怨“的”感同身受“之感?細數那些個“玩意兒”,你對Java的熱情是否還如初戀般“人生若只如初見”?
Java中對于鎖的實作真可謂是“百花齊放”,按照編程友好程度來說,美其名曰是Java提供了種類豐富的鎖,每種鎖因其特性的不同,在適當的場景下能夠展現出非常高的效率,
但是,從理解的難度上來講,其型別錯中復雜,主要原因是Java是按照是否含有某一特性來定義鎖的實作,如果不能正確理解其含義,了解其特性的話,往往都會深陷其中,難可自拔,
查詢過很多技術資料與相關書籍,對其介紹真可謂是“模棱兩可”,生怕我們搞懂了似的,但是這也是我們無法繞過去的一個“坎坎”,除非有其他的選擇,
作為一名Java Developer來說,正確了解和掌握這些鎖的機制和原理,需要我們帶著一些實際問題,通過特性將鎖進行分組歸類,才能真正意義上理解和掌握,
比如,在Java領域中,針對于不同場景提供的鎖,都用于解決什么問題?其實作方式是什么?各自又有什么特點,對應的應用有哪些?
帶著這些問題,今天我們就一起來盤一盤,Java領域中的鎖機制,盤點一下相關知識點,以及不同的鎖的適用場景,幫助我們更快捷的理解和掌握這項必備技術奧義,
關健術語

本文用到的一些關鍵詞語以及常用術語,主要如下:
- 執行緒調度(Thread Scheduling ):系統分配處理器使用權的程序,主要調度方式有兩種,分別是協同式執行緒調度(Cooperative Threads-Scheduling)和搶占式執行緒調度(Preemptive Threads-Scheduling),
- 執行緒切換(Thread Switch ):主要是指在并發程序中,多執行緒之間會對背景關系進行切換資源,并交叉執行的一種并發機制,
- 指令重排(Command Reorder ): 指編譯器或處理器為了優化性能而采取的一種手段,在不存在資料依賴性情況下(如寫后讀,讀后寫,寫后寫),調整代碼執行順序,
- 記憶體屏障(Memory Barrier): 也稱記憶體柵欄,記憶體柵障,屏障指令等, 是一類同步屏障指令,是CPU或編譯器在對記憶體隨機訪問的操作中的一個同步點,使得此點之前的所有讀寫操作都執行后才可以開始執行此點之后的操作,
基本概述

縱觀Java領域中“五花八門”的鎖,我們可以依據Java記憶體模型的作業機制,來具體分析一下對應問題的提出和表現,這也不失為打開Java領域中鎖機制的“敲門磚”,
從本質上講,鎖是一種協調多個行程 或者多個執行緒對某一個資源的訪問的控制機制,
一.計算機運行模型
計算機運行模型主要是描述計算機系統體系結構的基本模型,一般主要是指CPU處理器結構,

在計算機體系結構中,中央處理器(CPU,Central Processing Unit)是一塊超大規模的集成電路,是一臺計算機的運算核心(Core)和控制核心( Control Unit),它的功能主要是解釋計算機指令以及處理計算機軟體中的資料,
一個計算能夠運行起來,主要是依靠CPU來負責執行我們的輸入指令的,通常情況下,我們都把這些指令統稱為程式,
一般CPU決定著程式的運行速度,可以看出CPU對程式的執行有很重要的作用,但是一個計算機程式的運行快慢并不是完全由CPU決定,除了CPU還有記憶體、閃存等,
由此可見,一個CPU主要由控制單元,算術邏輯單元和暫存器單元等3個部分組成,其中:

- 控制單元( Control Unit): 屬于CPU的控制指揮中心,主要負責指揮CPU作業,通過向算術邏輯單元和暫存器單元來發送控制指令達到控制效果,
- 算術邏輯單元(Arithmetic Logic Unit, ALU): 主要負責執行運算,一般是指算術運算和邏輯運算,主要是依據控制單元發送過來的指令進行處理,
- 暫存器單元(Register Unit): 主要用于存盤臨時資料,保存著等待處理和已經處理的資料,
一般來說,暫存器單元是為了減少CPU對記憶體的訪問次數,提升資料讀取性能而提出的,CPU中的暫存器單元主要分為通用暫存器和專用暫存器兩個種,其中:
- 通用暫存器:主要用于臨時存放CPU正在使用的資料,
- 專用暫存器:主要用于臨時存放類似指令暫存器和程式計數器等CPU中專有用途的資料,其中:
- 指令暫存器:用于存盤正在執行的指令
- 程式計數器: 保存等待執行的指令地址
簡單來說,CPU與主存盤器主要是通過總線來進行通信,CPU通過控制單元來操作主存中的資料,而CPU與其他設備的通信都是由控制來實作,
綜上所述,我們便可以得到一個計算機記憶體模型的大致雛形,接下來,我們便來一起盤點決議是計算機記憶體模型的基本奧義,
二.計算機記憶體模型
計算機記憶體模型一般是指計算系統底層與編程語言之間的約束規范,主要是描述計算機程式與共享存盤器訪問的行為特征表現,

根據介紹計算機運行模型來看,計算機記憶體模型可以幫助以及指導我們理解Java記憶體模型,主要在如下的兩個方面:
- 首先,系統底層希望能夠對程式進行更多的優化策略,一般主要是針對處理器和編譯器,從而提高運行性能,
- 其次,為編程語言帶來了更多的可編程性問題,主要是復雜的記憶體模型會有更多的約束,從而增加了程式設計的編程難度,
由此可見,記憶體模型用于定義處理器間的各層快取與共享記憶體的同步機制,以及執行緒與記憶體之間互動的規則,
在作業系統層面,記憶體主要可以分為物理記憶體與虛擬記憶體的概念,其中:
- 物理記憶體(Physical Memory): 通常指通過安裝記憶體條而獲得的臨時儲存空間,主要作用是在計算機運行時為作業系統和各種程式提供臨時儲存,常見的物理記憶體規格有256M、512M、1G、2G等,
- 虛擬記憶體(Virtual Memory):計算機系統記憶體管理的一種技術,它使得應用程式認為它擁有連續可用的記憶體(一個連續完整的地址空間),它通常是被分隔成多個物理記憶體碎片,還有部分暫時存盤在外部磁盤存盤器上,在需要時進行資料交換,
一般情況下,當物理記憶體不足時,可以用虛擬記憶體代替, 在虛擬記憶體出現之前,程式尋址用的都是物理地址,
從常見的存盤介質來看,主要有:暫存器(Register),高速快取(Cache),隨機存取存盤器(RAM),只讀存盤器(ROM)等4種,按照讀取快慢的順序是:Register>Cache>RAM>ROM,其中:
- 暫存器(Register): CPU處理器的一部分,主要分為通用暫存器和專用暫存器,
- 高速快取(Cache):用于減少 CPU 處理器訪問記憶體所需平均時間的部件,一般是指L1/L2/L3層高級快取,
- 隨機存取存盤器(Random Access Memory,RAM):與CPU直接交換資料的內部存盤器,它可以隨時讀寫,而且速度很快,通常作為作業系統或其他正在運行中的程式的臨時資料存盤媒介,
- 只讀存盤器(Read-Only Memory,ROM):所存盤的資料通常都是裝入主機之前就寫好的,在作業的時候只能讀取而不能像隨機存盤器那樣隨便寫入,
由于CPU的運算速度比主存(物理記憶體)的存取速度快很多,為了提高處理速度,現代CPU不直接和主存進行通信,而是在CPU和主存之間設計了多層的Cache(高速快取),越靠近CPU的高速快取越快,容
量也越小,
按照資料讀取順序和與CPU內核結合的緊密程度來看,大多數采用多層快取策略,最經典的就三層高速快取架構,
也就是我們常說的,CPU高速快取有L1和L2高速快取(即一級高速快取和二級快取高速),部分高端CPU還具有L3高速快取(即三級高速快取):

CPU內核讀取資料時,先從L1高速快取中讀取,如果沒有命中,再到L2、L3高速快取中讀取,假如這些高速快取都沒有命中,它就會到主存中讀取所需要的資料,
每一級高速快取中所存盤的資料都是下一級高速快取的一部分,越靠近CPU的高速快取讀取越快,容量也越小,
當然,系統還擁有一塊主存(即主記憶體),由系統中的所有CPU共享,擁有L3高速快取的CPU,CPU存取資料的命中率可達95%,也就是說只有不到5%的資料需要從主存中去存取,
因此,高速快取大大縮小了高速CPU內核與低速主存之間的速度差距,基本體現在如下:
- L1高速快取:最接近CPU,容量最小、存取速度最快,每個核上都有一個L1高速快取,
- L2高速快取:容量更大、速度低些,在一般情況下,每個內核上都有一個獨立的L2高速快取,
- L3高速快取:最接近主存,容量最大、速度最低,由在同一個CPU芯片板上的不同CPU內核共享,
總結來說,CPU通過高速快取進行資料讀取有以下優勢:
- 寫緩沖區可以保證指令流水線持續運行,可以避免由于CPU停頓下來等待向記憶體寫入資料而產生的延遲,
- 通過以批處理的方式重繪寫緩沖區,以及合并寫緩沖區中對同一記憶體地址的多次寫,減少對記憶體總線的占用,
綜上所述,一般來說,對于單執行緒程式,編譯器和處理器的優化可以對編程開發足夠透明,對其優化的效果不會影響結果的準確性,
而在多執行緒程式來說,為了提升性能優化的同時又達到兼顧執行結果的準確性,需要一定程度上記憶體模型規范,
由于經常會采用多層快取策略,這就導致了一個比較經典的并發編程三大問題之一的共享變數的可見性問題,除了可見性問題之外,當然還有原子性問題和有序性問題,
由此來看,在計算機記憶體模型中,主要可以提出主存和作業記憶體的概念,其中:
- 主存:一般指的物理記憶體,主要是指RAM隨機存取存盤器和ROM只讀存盤器等
- 作業記憶體:一般指暫存器,還有以及我們說的三層高速快取策略中的L1/L2/L3層高級快取Cache等
在Java領域中,為了解決這一系列問題,特此提出了Java記憶體模型,接下來,我們就來一看看Java記憶體模型的作業機制,
三.Java記憶體模型
Java記憶體模型主要是為了解決并發編程的可見性問題,原子性問題和有序性問題等三大問題,具有跨平臺性,

JMM最初由JSR-133(Java Memory Model and ThreadSpecification)檔案描述,JMM定義了一組規則或規范,該規范定義了一個執行緒對共享變數寫入時,如何確保對另一個執行緒是可見的,
Java記憶體模型(Java Memory Model JMM)指的是Java HotSpot(TM) VM 虛擬機定義的一種統一的記憶體模型,將底層硬體以及作業系統的記憶體訪問差異進行封裝,使得Java程式在不同硬體以及作業系統上執行都能達到相同的并發效果,
Java記憶體模型對于記憶體的描述主要體現在三個方面:
- 首先,描述程式各個變數之間關系,主要包括實體域,靜態域,資料元素等,
- 其次,描述了在計算機系統中將變數存盤到記憶體以及從記憶體中獲取變數的底層細節,主要包括針對某個執行緒對于共享變數的進行操作時,如何通知其他執行緒(涉及執行緒間如何通信)
- 最后,描述了多個執行緒對于主存中的共享資源的安全訪問問題,
一般來說,Java記憶體模型在對記憶體的描述上,我們可以依據是編譯時分配還是運行時分配,是靜態分配還是動態分配,是堆上分配還是堆疊上分配等角度來進行對比分析,
從Java HotSpot(TM) VM 虛擬機的整體結構上來看,記憶體區域可以分為執行緒私有區,執行緒共享區,直接記憶體等內容,其中:

- 執行緒私有區(Thread Local):主要包括程式計數器、虛擬機堆疊、本地方法區,其中執行緒私有資料區域生命周期與執行緒相同, 依賴用戶執行緒的啟動/結束 而 創建/銷毀,
- 執行緒共享區(Thread Shared):主要包括JAVA 堆、方法區,其中,執行緒共享區域隨虛擬機的啟動/關閉而創建/銷毀,
- 直接記憶體(Driect Memory):不會受Java HotSpot(TM) VM 虛擬機中的GC影響,并不是JVM運行時資料區的成員,
根據執行緒私有區中包含的資料(程式計數器、虛擬機堆疊、本地方法區)來具體分析看,其中:
- 程式計數器(Program Counter Register ):一塊較小的記憶體空間, 是當前執行緒所執行的位元組碼的行號指示器,每條執行緒都要有一個獨立的程式計數器,而且是唯一一個在虛擬機中沒有規定任何OutOfMemoryError情況的區域,
- 虛擬機堆疊(VM Stack):是描述Java方法執行的記憶體模型,在方法執行的同時都會創建一個堆疊幀用于存盤區域變數表、運算元堆疊、動態鏈接、方法出口等資訊,
- 本地方法區(Native Method Stack):和Java Stack作用類似, 區別是虛擬機堆疊為執行Java方法服務, 而本地方法堆疊則為Native方法服務,
根據執行緒共享區中包含的資料(JAVA 堆、方法區)來具體分析看,其中:
- JAVA 堆(Heap):是被執行緒共享的一塊記憶體區域,創建的物件和陣列都保存在Java堆記憶體中,也是垃圾收集器進行垃圾收集的最重要的記憶體區域,
- 方法區(Method Area):是指Java HotSpot(TM) VM 虛擬機把GC分代收集擴展至方法區,Java HotSpot(TM) VM 的垃圾收集器就可以像管理Java堆一樣管理這部分記憶體, 而不必為方法區開發專門的記憶體管理器,其中這里需要注意的是:
- 在JDK1.8之前,使用永久代(Permanent Generation), 用于存盤被JVM加載的類資訊、常量、靜態變數、即時編譯器編譯后的代碼等資料. , 即使用Java堆的永久代來實作方法區, 主要是因為永久帶的記憶體回收的主要目標是針對常量池的回收和型別的卸載, 其收益一般很小,
- 在JDK1.8之后,永久代已經被移除,被一個稱為“元資料區(Metadata Area)”的區域所取代,元空間(Metadata Space)的本質和永久代類似,最大的區別在于:元空間并不在虛擬機中,而是使用本地記憶體,默認情況下,元空間的大小僅受本地記憶體限制,類的元資料放入 Native Memory, 字串池和類的靜態變數放入Java堆中,這樣可以加載多少類的元資料由系統的實際可用空間來控制,
這里對執行緒共享區和程私有區其細節,就暫時不做展開,但是我們可以簡單地看出,對于Java領域中的記憶體分配,這兩者之間已經幫助我們做了具體區分,
在繼續后續問題探索之前,我們一起來思考一個問題:按照線性思維來看,一個Java程式從程式撰寫到編譯,編譯到運行,運行到執行等程序來說,究竟是先入堆還是先入堆疊呢 ?
這個問題,其實我在看Java HotSpot(TM) VM 虛擬機相關知識的時候,一直有這樣的個疑慮,但是其實這樣的表述是不準確的,這需要結合編譯原理相關的知識來具體分析,
按照編譯原理的觀點,從Java記憶體分配策略來看,程式運行時的記憶體分配有三種策略,其中:

- 靜態存盤分配:靜態存盤分配要求在編譯時能知道所有變數的存盤要求,指在編譯時,就能確定每個資料在運行時的存盤空間,因而在編譯時就可以給他們分配固定的記憶體空間,這種分配策略要求程式代碼中不允許有可變資料結構的存在,也不允許有嵌套或者遞回的結構出現,因為它們都會導致編譯程式無法計算準確的存盤空間需求,
- 堆疊式存盤分配:堆疊式存盤分配要求在程序的入口處必須知道所有的存盤要求,也可稱為動態存盤分配,是由一個類似于堆疊的運行堆疊來實作的,和靜態存盤分配相反,在堆疊式存盤方案中,程式對資料區的需求在編譯時是完全未知的,只有到運行的時候才能夠知道,也就是規定在運行中進入一個程式模塊時,必須知道該程式模塊所需的資料區大小才能夠為其分配記憶體,堆疊式存盤分配按照先進后出的原則進行分配,
- 堆式存盤分配:堆式存盤分配則專門負責在編譯時或運行時模塊入口處都無法確定存盤要求的資料結構的記憶體分配,比如可變長度串和物件實體,堆由大片的可利用塊或空閑塊組成,堆中的記憶體可以按照任意順序分配和釋放,
也就是說,在Java領域中,一個Java程式從程式撰寫到編譯,編譯到運行,運行到執行等程序來說,單純考慮是先入堆還是入堆疊的問題,在這里得到了答案,
從整體上來看,Java記憶體模型主要考慮的事情基本與主存,執行緒本地記憶體,共享變數,變數副本,執行緒等概念息息相關,其中:
- 從主存與執行緒本地記憶體的關系來看 : 主存主要保存Java程式中的共享變數,其中主存不保存區域變數和方法引數串列;而執行緒本地記憶體主要保存Java程式中的共享變數的變數副本,
- 從執行緒與執行緒本地記憶體的關系來看:每個執行緒都會維護一個自己專屬的本地記憶體,不同執行緒之間互相不可直接通信,其執行緒之間的通信就會涉及共享變數可見性的問題,
在Java記憶體模型中,一般來說主要提供volatile,synchronized,final以及鎖等4種方式來保證變數的可見性問題,其中:
- 通過volatile關鍵詞實作: 利用volatile修飾宣告時,變數一旦有更改都會被立即同步到主存中,當執行緒需要使用這個變數時,需要從主存中重繪到作業記憶體中,
- 通過synchronized關鍵詞實作:利用synchronized修飾宣告時,當一個執行緒釋放一個鎖,強制重繪作業記憶體中的變數到主存中,當另外一個執行緒需要使用此鎖時,會強制重新載入變數值,
- 通過final關鍵詞實作:利用final修飾宣告時,變數一旦初始化完成,Java中的執行緒都可以看到這個變數,
- 通過JDK中鎖實作:當一個執行緒釋放一個鎖,強制重繪作業記憶體中的變數到主存中,當另外一個執行緒需要使用此鎖時,會強制重新載入變數值,
實際上,相比之下,Java記憶體模型還引入了一個作業記憶體的概念來幫助我們提升性能,而且JMM提供了合理的禁用快取以及禁止重排序的方法,所以其核心的價值在于解決可見性和有序性,
其中,需要特別注意的是,其主存和作業記憶體的區別:
- 主存: 可以在計算機記憶體模型說是物理記憶體,對應到Java記憶體模型來講,是Java HotSpot(TM) VM 虛擬機中虛擬記憶體的一部分,
- 作業記憶體:在計算機記憶體模型內是指CPU快取,一般是指暫存器,還有以及我們說的三層高速快取策略中的L1/L2/L3層高級快取;對應到Java記憶體模型來講,主要是三層高速快取Cache和暫存器,
綜上所述,我們對Java記憶體模型的探討算是水到渠成了,但是Java記憶體模型也提出了一些規范,接下來,我們就來看看Happen-Before 關系原則,
四.Java一致性模型指導原則
Java一致性模型指導原則是指制定一些規范來將復雜的物理計算機的系統底層封裝到JVM中,從而向上提供一種統一的記憶體模型語意規則,一般是指Happens-Before規則,

Happen-Before 關系原則,是 Java 記憶體模型中保證多執行緒操作可見性的機制,也是對早期語言規范中含糊的可見性概念的一個精確定義,其行為依賴于處理器本身的記憶體一致性模型,
Happen-Before 關系原則主要規定了Java記憶體在多執行緒操作下的順序性,一般是指先發生操作的執行結果對后續發生的操作可見,因此稱其為Java一致性模型指導原則,
由于Happen-Before 關系原則是向上提供一種統一的記憶體模型語意規則,它規范了Java HotSpot(TM) VM 虛擬機的實作,也能為上層Java Developer描述多執行緒并發的可見性問題,
在Java領域中,Happen-Before 關系原則主要有8種,具體如下:
- 單執行緒原則:執行緒內執行的每個操作,都保證 happen-before 后面的操作,這就保證了基本的程式順序規則,這是開發者在書寫程式時的基本約定,
- 鎖原則:對于一個鎖的解鎖操作,保證 happen-before 加鎖操作,
- volatile原則:對于 volatile 變數,對它的寫操作,保證 happen-before 在隨后對該變數的讀取操作,
- 執行緒Start原則:類似執行緒內部操作的完成,保證 happen-before 其他 Thread.start() 的執行緒操作原則,
- 執行緒Join原則:類似執行緒內部操作的完成,保證 happen-before 其他 Thread.join() 的執行緒操作原則,
- 執行緒Interrupt原則:類似執行緒內部操作的完成,保證 happen-before 其他 Thread.interrupt() 的執行緒操作原則,
- finalize原則: 物件構建完成,保證 happen-before 于 finalizer 的開始動作,
- 傳遞原則: Happen-Before 關系是存在著傳遞性的,如果滿足 A happen-before B 和 B happen-before C,那么 A happen-before C 也成立,
對于Happen-Before 關系原則來說,而不是簡單地線性思維的前后順序問題,是因為它不僅僅是對執行時間的保證,也包括對記憶體讀、寫操作順序的保證,僅僅是時鐘順序上的先后,并不能保證執行緒互動的可見性,
在Java HotSpot(TM) VM 虛擬機內部的運行時資料區,但是真正程式執行,實際是要跑在具體的處理器內核上,簡單來說,把本地變數等資料從記憶體加載到快取、暫存器,然后運算結束寫回主記憶體,
總的來說,JMM 內部的實作通常是依賴于記憶體屏障,通過禁止某些重排序的方式,提供記憶體可見性保證,也就是實作了各種 happen-before 規則,與此同時,更多復雜度在于,需要盡量確保各種編譯器、各種體系結構的處理器,都能夠提供一致的行為,
五.Java指令重排
Java指令重排是指在執行程式時為了提高性能,編譯器和處理器常常會對指令做重排序的一種防護措施機制,

我們在實際開發作業中撰寫代碼時候,是按照一定的代碼的思維和習慣去編排和組織代碼的,但是實際上,編譯器和CPU執行的順序可能會代碼順序產生不一致的情況,
畢竟,編譯器和CPU會對我們撰寫的程式代碼自身做一定程度上的優化再去執行,以此來提高執行效率,因此提出了指令重排的機制,
一般來說,我們在程式中撰寫的每一個行代碼其實就是程式指令,按照線性思維方式來看,這些指令按道理是一行行代碼存在的順序去執行的,只有上一行代碼執行完畢,下一行代碼才會被執行,這就說明代碼的執行有一定的順序,
但是這樣的順序,對于程式的執行時間上來看是有一定的耗時的,為了加快代碼的執行效率,一般會引入一種流水線技術的方式來解決這個問題,就像Jenkins 流水線部署機制的撰寫那樣,
但是流水線技術的本質上,是把每一個指令拆成若干個部分,在同一個CPU的時間內使其可以執行多個指令的不同部分,從而達到提升執行效率的目的,主要體現在:
- 獲取指令階段: 主要使用指令通道和指令暫存器,一般是在CPU處理器主導
- 編譯指令階段:主要使用指令編譯器,一般是在編譯器主導
- 執行指令階段:主要使用執行單元和資料通道,相對來說像是從記憶體在主導
一般來說,指令從排會涉及到CPU,編譯器,以及記憶體等,因此指令重排序的型別大致可以分為 編譯器指令重排,CPU指令重排,記憶體指令重排,其中:
- 編譯器指令重排:編譯器在不改變單執行緒程式語意的前提下,可以重新安排陳述句的執行順序
- CPU指令重排:現代處理器采用了指令級并行技術(Instruction-Level Parallelism, ILP)來將多條指令重疊執行,如果不存在資料依賴性,處理器可以改變陳述句對應機器指令的執行順序,
- 記憶體指令重排:由于處理器使用快取和讀/寫緩沖區,其加載和存盤操作看上去類似亂序執行的情況,
在Java領域中,指令重排的原則是不能影響程式在單執行緒下的執行的準確性,但是在多執行緒的情況下,可能會導致程式執行出現錯誤的情況,主要是依據Happen-Before 關系原則來組織部重排序,其核心就是使用記憶體屏障來實作,通過記憶體屏障可以堆記憶體進行順序約束,而且作用于執行緒,
由于Java有不同的編譯器和運行時環境,對應起來看,Java指令重排主要發生在編譯階段和運行階段,而編譯階段對應的是編譯器,運行階段對應著CPU,其中:

- 編譯階段指令重排:
- 1?? 通用描述:源代碼->機器碼的指令重排: 源代碼經過編譯器變成機器碼,而機器碼可能被重排
- 2?? Java描述:Java源檔案->Java位元組碼的指令重排: Java源檔案被javac編譯后變成Java位元組碼,其位元組碼可能被重排
- 運行階段指令重排:
- 1?? 通用描述:機器碼->CPU處理器的指令重排:機器碼經過CPU處理時,可能會被CPU重排才執行
- 2?? Java描述:Java位元組碼->Java執行器的指令重排: Java位元組碼被Java執行器執行時,可能會被CPU重排才執行
既然設定記憶體屏障,可以確保多CPU的高速快取中的資料與記憶體保持一致性, 不能確保記憶體與CPU快取資料一致性的指令也不能重排,記憶體屏障正是通過阻止屏障兩邊的指令重排序來避免編譯器和硬體的不正確優化而提出的一種解決辦法,
但是記憶體屏障的是需要考慮CPU的架構方式,不同硬體實作記憶體屏障的方式不同,一般以常見Intel CPU來看,主要有:
- 1?? lfence屏障: 是一種Load Barrier 讀屏障,
- 2?? sfence屏障: 是一種Store Barrier 寫屏障 ,
- 3?? mfence屏障:是一種全能型的屏障,具備ifence和sfence的能力 ,
- 4?? Lock前綴,Lock不是一種記憶體屏障,但是它能完成類似記憶體屏障的功能,Lock會對CPU總線和高速快取加鎖,可以理解為CPU指令級的一種鎖,
在Java領域中,Java記憶體模型屏蔽了這種底層硬體平臺的差異,由JVM來為不同的平臺生成相應的機器碼,

從廣義上的概念定義看,Java中的記憶體屏障一般主要有Load和Store兩類:
- 1?? 對Load Barrier來說,在讀指令前插入讀屏障,可以讓高速快取中的資料失效,重新從主記憶體加載資料
- 2?? 對Store Barrier來說,在寫指令之后插入寫屏障,能讓寫入快取的最新資料寫回到主記憶體
從具體的使用方式來看,Java中的記憶體屏障主要有以下幾種方式:
- 1?? 通過 synchronized關鍵字包住的代碼區域:當執行緒進入到該區域讀取變數資訊時,保證讀到的是最新的值,
- a. 在同步區內對變數的寫入操作,在離開同步區時就將當前執行緒內的資料重繪到記憶體中,
- b. 對資料的讀取也不能從快取讀取,只能從記憶體中讀取,保證了資料的讀有效性.這也是會插入StoreStore屏障的緣故, - 2?? 通過volatile關鍵字修飾變數:當對變數的寫操作,會插入StoreLoad屏障,
- 3?? 其他的設定方式,一般需要通過Unsafe這個類來執行,主要是:
- a. Unsafe.putOrderedObject():類似這樣的方法,會插入StoreStore記憶體屏障
- b. Unsafe.putVolatiObject() 類似這樣的方法,會插入StoreLoad屏障
綜上所述,一般來說volatile關健字能保證可見性和防止指令重排序,也是我們最常見提到的方式,
六.Java并發編程的三宗罪
Java并發編程的三宗罪主要是指原子性問題、可見性問題和有序性問題等三大問題,

在介紹Java記憶體模型時,我們都說其核心的價值在于解決可見性和有序性,以及還有原子性等,那么對其總結來說,就是Java并發編程的三宗罪,其中:
- 原子性問題:就是“不可中斷的一個或一系列操作”,是指不會被執行緒調度機制打斷的操作,這種操作一旦開始,就一直運行到結束,中間不會有任何執行緒的切換,
- 可見性問題:一個執行緒對共享變數的修改,另一個執行緒能夠立刻可見,我們稱該共享變數具備記憶體可見性,
- 有序性問題:指程式按照代碼的先后順序執行,如果程式執行的順序與代碼的先后順序不同,并導致了錯誤的結果,即發生了有序性問題,
但是,這里我們需要知道,Java記憶體模型是如何解決這些問題的?主要體現如下幾個方面:
- 解決原子性問題:Java記憶體模型通過read、load、assign、use、store、write來保證原子性操作,此外還有lock和unlock,直接對應著synchronized關鍵字的monitorenter和monitorexit位元組碼指令,
- 解決可見性問題:Java保證可見性通過volatile、final以及synchronized,鎖來實作,
- 解決有序性問題:由于處理器和編譯器的重排序導致的有序性問題,Java主要可以通過volatile、synchronized來保證,
一定意義上來講,一般在Java并發編程中,其實加鎖可以解決一部分問題,除此之外,我們還需要考慮執行緒饑餓問題,資料競爭問題,競爭條件問題以及死鎖問題,通過綜合分析才能得到意想不到的結果,
綜上所述,我們在理解Java領域中的鎖時,可以以此作為一個考量標準之一,來幫助和方便我們更快理解和掌握并發編程技術,
七.Java執行緒饑餓問題
Java執行緒饑餓問題是指長期無法獲取共享資源或搶占CPU資源而導致執行緒無法執行的現象,

在Java并發編程的程序中,特別是開啟執行緒數過多,會遇到某些執行緒貪婪地把CPU資源占滿,導致某些執行緒分配不到CPU而沒有辦法執行,
在Java領域中,對于執行緒饑餓問題,可以從以下幾個方面來看:
- 互斥鎖synchronized饑餓問題:在使用synchronized對資源進行加鎖時,不斷有大量的執行緒去競爭獲取鎖,那么就可能會引發執行緒饑餓問題,主要是synchronized只是加鎖,沒有要求公平性導致的,
- 執行緒優先級饑餓問題:Java中每個執行緒都有自己的優先級,一般情況下使用默認優先級,但是由于執行緒優先級不同,也會引起執行緒饑餓問題,
- 執行緒自旋饑餓問題: 主要是在Java并發操作中,會使用自旋鎖,由于鎖的核心的自旋操作,會導致大量執行緒自旋,也會引起執行緒饑餓問題,
- 等待喚醒饑餓問題: 主要是因為JVM中wait和notify實作不同,比方說Java HotSpot(TM) VM 虛擬機是一種先入先出結構,也會引起執行緒饑餓問題,
針對上述的饑餓問題,為了解決它,JDK內部實作一些具備公平性質的鎖,可以直接使用,所以,解決執行緒饑餓問題,一般是引入佇列,也就是排隊處理,最典型的有ReentrantLock,
綜上所述,這不就是為我們掌握和理解Java中的鎖機制時,需要考慮Java執行緒饑餓問題,
八.Java資料競爭問題
Java資料競爭問題是指至少存在兩個執行緒去讀寫某個共享記憶體,其中至少一個執行緒對其共享記憶體進行寫操作,

對于資料競爭問題,最簡單的理解就是,多個執行緒在同時對于共享記憶體的進行寫操作時,在寫的程序中,其他的執行緒讀到資料是記憶體資料中非正確預期的,
產生資料競爭的原因,一個CPU在任意時刻只能執行一條指令,但是對其某個記憶體中的寫操作可能會用到若干條件機器指令,從而導致在寫的程序中還沒完全修改完記憶體,其他執行緒去讀取資料,從而導致結果不可預知,從而引發資料競爭問題,這個情況有點像MySQL資料中并發事務引起的臟讀情況,
在Java領域中,解決資料競爭問題的方式一般是把共享記憶體的更新操作進行原子化,同時也保證記憶體的可見性,
針對上述的饑餓問題,為了解決它,JDK內部實作一系列的原子類,比如AtomicReference類等,但是主要可以采用CAS+自旋鎖的方式來實作,
綜上所述,這不就是為我們掌握和理解Java中的鎖機制時,需要考慮Java資料競爭問題,
九.Java競爭條件問題
Java競爭條件問題是指代碼在執行臨界區產生競爭條件,主要是因為多個執行緒不同的執行順序以及執行緒并發的交叉執行導致執行結果與預期不一致的情況,

對于競爭條件問題,其中臨界區是一塊代碼區域,其實說白了就是我們自己寫的邏輯代碼,由于沒有考慮位,從而引發的多個執行緒不同的執行順序以及執行緒并發的交叉執行導致執行結果與預期不一致的情況,
產生競爭條件問題的主要原因,一般主要有執行緒執行順序的不確定性和并發機制導致背景關系切換等兩個原因導致競爭條件問題,其中:
- 執行緒執行順序的不確定性:這個執行緒調度的作業方式有關,現在大部分計算機的作業系統都是搶占方式的調度方式,所有的任務調度由作業系統來完全控制,執行緒的執行順序不一定是按照編碼順序的,主要有作業系統調度演算法決定,
- 并發機制導致背景關系切換:在并發的多執行緒的程式中,多個執行緒會導致進行背景關系的資源切換,并且交叉執行,從而并發機制自身也會引起競爭條件問題,
在Java領域中,解決競爭條件問題的方式一般是把臨界區進行原子化,保證臨界區的源自性,保證了臨界區捏只有一個執行緒,從而避免競爭產生,
針對上述的饑餓問題,為了解決它,JDK內部實作一系列的原子類或者說直接使用synchronized來宣告,均可實作,
綜上所述,這不就是為我們掌握和理解Java中的鎖機制時,需要考慮Java競爭條件問題,
十.Java死鎖問題
Java死鎖問題主要是指一種有兩個或者兩個以上的執行緒或者行程構成一個無限互相等待的環形狀態的情況,不是一種鎖概念,而是一種執行緒狀態的表征描述,

一般為了保證執行緒安全問題,我們都會想著給會使用加鎖機制來確保執行緒安全,但如果過度地使用加鎖,則可能導致鎖順序死鎖(Lock-Ordering Deadlock),
或者有的場景我們使用執行緒池和信號量等來限制資源的使用,但這些被限制的行為可能會導致資源死鎖(Resource DeadLock),
Java死鎖問題的主要體現在以下幾個方面:
- 1?? Java應用程式不具備MySQL資料庫服務器的本地事務,無法檢測一組事務中是否有死鎖的發生,
- 2?? 在Java程式中,如果過度地使用加鎖,輕則導致程式回應時間變長,系統吞吐量變小,重則導致應用中的某一個功能直接失去回應能力無法提供服務,
當然,死鎖問題的產生也必須具備以及同時滿足以下幾個條件:
- 互斥條件:資源具有排他性,當資源被一個執行緒占用時,別的執行緒不能使用,只能等待,
- 阻塞不釋放條件: 某個執行緒或者執行緒請求某個資源而進入阻塞狀態,不會釋放已經獲取的資源,
- 占有并等待條件: 某個執行緒或者執行緒應該至少占有一個資源,等待獲取另外一個資源,該資源被其他執行緒或者執行緒霸占,
- 非搶占條件: 不可搶占,資源請求者不能強制從資源占有者手中搶奪資源,資源只能由占有者主動釋放,
- 環形條件: 回圈等待,多個執行緒存在環路的鎖依賴關系而永遠等待下去,
對于死鎖問題,一般都是需要編程開發人員人為去干預和防止的,只是需要一些措施區規范處理,主要可以分為事前預防和事后處理等2種方式,其中:
- 事前預防: 一般是保證鎖的順序化,資源合并處理,以及避免嵌套鎖等,
- 事后處理: 一般是對鎖設定超時機制,在死鎖發生時搶占鎖資源,以及撤銷執行緒機制等,
除了有死鎖的問題,當然還有活鎖問題,主要是因為某些邏輯導致一直在做無用功,使得執行緒無法正確執行的情況,
應用分析
在Java領域中,我們可以將鎖大致分為基于Java語法層面(關鍵詞)實作的鎖和基于JDK層面實作的鎖,

單純從Java對其實作的方式上來看,我們大體上可以將其分為基于Java語法層面(關鍵詞)實作的鎖和基于JDK層面實作的鎖,其中:
- 基于Java語法層面(關鍵詞)實作的鎖,主要是根據Java語意來實作,最典型的應用就是synchronized,
- 基于JDK層面實作的鎖,主要是根據統一的AQS基礎同步器來實作,最典型的有ReentrantLock,
需要特別注意的是,在Java領域中,基于JDK層面的鎖通過CAS操作解決了并發編程中的原子性問題,而基于Java語法層面實作的鎖解決了并發編程中的原子性問題和可見性問題,
單純從Java對其實作的方式上來看,我們大體上可以將其分為基于Java語法層面(關鍵詞)實作的鎖和基于JDK層面實作的鎖,其中:
- 基于Java語法層面(關鍵詞)實作的鎖,主要是根據Java語意來實作,最典型的應用就是synchronized,
- 基于JDK層面實作的鎖,主要是根據統一的AQS基礎同步器來實作,最典型的有ReentrantLock,
需要特別注意的是,在Java領域中,基于JDK層面的鎖通過CAS操作解決了并發編程中的原子性問題,而基于Java語法層面實作的鎖解決了并發編程中的原子性問題和可見性問題,
而從具體到對應的Java執行緒資源來說,我們按照是否含有某一特性來定義鎖,主要可以從如下幾個方面來看:

- 從加鎖物件角度方面上來看,執行緒要不要鎖住同步資源 ? 如果是需要加鎖,鎖住同步資源的情況下,一般稱其為悲觀鎖;否則,如果是不需要加鎖,且不用鎖住同步資源的情況就屬于為樂觀鎖,
- 從獲取鎖的處理方式上來看,假設鎖住同步資源,其對該執行緒是否進入睡眠狀態或者阻塞狀態?如果會進入睡眠狀態或者阻塞狀態,一般稱其為互斥鎖,否則,不會進入睡眠狀態或者阻塞狀態屬于一種非阻塞鎖,即就是自旋鎖,
- 從鎖的變化狀態方面來看,多個執行緒在競爭資源的流程細節上是否有差別?
- 1?? 對于不會鎖住資源,多個執行緒只有一個執行緒能修改資源成功,其他執行緒會依據實際情況進行重試,即就是不存在競爭的情況,一般屬于無鎖,
- 2?? 對于同一個執行緒執行同步資源會自動獲取鎖資源,一般屬于偏向鎖,
- 3?? 對于多執行緒競爭同步資源時,沒有獲取到鎖資源的執行緒會自旋等待鎖釋放,一般屬于輕量級鎖,
- 4?? 對于多執行緒競爭同步資源時,沒有獲取到鎖資源的執行緒會阻塞等待喚醒,一般屬于重量級鎖,
- 從鎖競爭時公平性上來看,多個執行緒在競爭資源時是否需要排隊等待?如果是需要排隊等待的情況,一般屬于公平鎖;否則,先插隊,然后再嘗試排隊的情況屬于非公平鎖,
- 從獲取鎖的操作頻率次數來看,一個執行緒中的多個流程是否可以獲取同一把鎖?如果是可以多次進行加鎖操作的情況,一般屬于可重入鎖,否則,可以多次進行加鎖操作的情況屬于非可重入鎖,
- 從獲取鎖的占有方式上來看,多個執行緒能不能共享一把鎖?如果是可以共享鎖資源的情況,一般屬于共享鎖;否則,獨占鎖資源的情況屬于排他鎖,
針對于上述描述的各種情況,這里就不做展開和贅述,看到這里只需要在腦中形成一個概念就行,后續會有專門的內容來對其進行分析和探討,
寫在最后

在上述的內容中,一般常規的概念中,我們很難會依據上述這些問題去認識和看待Java中的鎖機制,主要是在學習和查閱資料的時,大多數的論調都是零散和細分的,很難在我們的腦海中形成知識體系,
從本質上講,我們對鎖應該有一個認識,其主要是一種協調多個行程 或者多個執行緒對某一個資源的訪問的控制機制,是并發編程中最關鍵的一環,
接下來,對于上述內容做一個簡單的總結:
- 1?? 計算機運行模型主要是描述計算機系統體系結構的基本模型,一般主要是指CPU處理器結構,
- 2?? 計算機記憶體模型一般是指計算系統底層與編程語言之間的約束規范,主要是描述計算機程式與共享存盤器訪問的行為特征表現,
- 3?? Java記憶體模型主要是為了解決并發編程的可見性問題,原子性問題和有序性問題等三大問題,具有跨平臺性,
- 4?? Java一致性模型指導原則是指制定一些規范來將復雜的物理計算機的系統底層封裝到JVM中,從而向上提供一種統一的記憶體模型語意規則,一般是指Happens-Before規則,
- 5?? Java指令重排是指在執行程式時為了提高性能,編譯器和處理器常常會對指令做重排序的一種防護措施機制,
- 6?? Java并發編程的三宗罪主要是指原子性問題、可見性問題和有序性問題等三大問題,
- 7?? Java執行緒饑餓問題是指長期無法獲取共享資源或搶占CPU資源而導致執行緒無法執行的現象,
- 8?? Java資料競爭問題是指至少存在兩個執行緒去讀寫某個共享記憶體,其中至少一個執行緒對其共享記憶體進行寫操作,
- 9?? Java競爭條件問題是指代碼在執行臨界區產生競爭條件,主要是因為多個執行緒不同的執行順序以及執行緒并發的交叉執行導致執行結果與預期不一致的情況,
- ?? Java死鎖問題主要是指一種有兩個或者兩個以上的執行緒或者行程構成一個無限互相等待的環形狀態的情況,不是一種鎖概念,而是一種執行緒狀態的表征描述,
單純從Java對其實作的方式上來看,我們大體上可以將其分為基于Java語法層面(關鍵詞)實作的鎖和基于JDK層面實作的鎖,其中:
- 1?? 基于Java語法層面(關鍵詞)實作的鎖,主要是根據Java語意來實作,最典型的應用就是synchronized,
- 2?? 基于JDK層面實作的鎖,主要是根據統一的AQS基礎同步器來實作,最典型的有ReentrantLock,
綜上所述,我相信看到這里的時候,對Java領域中的鎖機制已經有一個基本的輪廓,后面會專門寫一篇內容來詳細介紹,敬請期待,
最后,技術研究之路任重而道遠,愿我們熬的每一個通宵,都撐得起我們想在這條路上走下去的勇氣,未來仍然可期,與君共勉!
著作權宣告:本文為博主原創文章,遵循相關著作權協議,如若轉載或者分享請附上原文出處鏈接和鏈接來源,
轉載請註明出處,本文鏈接:https://www.uj5u.com/ruanti/503491.html
標籤:其他
上一篇:設計模式之(7)——裝飾設計模式
