Java内存模型—JMM详解
目錄
什么是JMM?
JMM內存模型
內存交互操作
JMM三大特性
原子性
可見性
有序性
指令重排問題
處理器重排序與內存屏障
數據依賴性
as-if-serial語義?
volatile
CAS
ABA問題
什么是ABA問題
如何解決ABA問題
各種鎖的理解
公平鎖和非公平鎖
可重入鎖
自旋鎖?
什么是JMM?
JMM即為JAVA 內存模型(java memory model)。不存在的東西,是概念,是約定。因為在不同的硬件生產商和不同的操作系統下,內存的訪問邏輯有一定的差異,結果就是當你的代碼在某個系統環境下運行良好,并且線程安全,但是換了個系統就出現各種問題。Java內存模型,就是為了屏蔽系統和硬件的差異,讓一套代碼在不同平臺下能到達相同的訪問結果。即達到Java程序能夠“一次編寫,到處運行”。
內存模型描述了程序中各個變量(實例域、靜態域和數組元素)之間的關系,以及在實際計算機系統中將變量存儲到內存和從內存中取出變量這樣的底層細節
Java Memory?Model(Java內存模型), 圍繞著在并發過程中如何處理可見性、原子性、有序性這三個特性而建立的模型。
JMM從java 5開始的JSR-133發布后,已經成熟和完善起來。
JSR-133規范
即JavaTM內存模型與線程規范,由JSR-133專家組開發。本規范是JSR-176(定義了JavaTM平臺 Tiger(5.0)發布版的主要特性)的一部分。本規范的標準內容將合并到JavaTM語言規范、JavaTM虛擬機規范以及java.lang包的類說明中。
JSR-133中文版下載
該規范在Java語言規范里面指出了JMM是一個比較開拓性的嘗試,這種嘗試視圖定義一個一致的、跨平臺的內存模型,但是它有一些比較細微而且很重要的缺點。它提供大范圍的流行硬件體系結構上的高性能JVM實現,現在的處理器在它們的內存模型上有著很大的不同,JMM應該能夠適合于實際的盡可能多的體系結構而不以性能為代價,這也是Java跨平臺型設計的基礎。
其實Java語言里面比較容易混淆的關鍵字主要是synchronized和volatile,也因為這樣在開發過程中往往開發者會忽略掉這些規則,這也使得編寫同步代碼比較困難。
JSR133本身的目的是為了修復原本JMM的一些缺陷而提出的。
?
JMM內存模型
JMM規定了所有的變量都存儲在主內存(Main Memory)中。每個線程還有自己的工作內存(Working Memory),線程的工作內存中保存了該線程使用到的變量的主內存的副本拷貝,線程對變量的所有操作(讀取、賦值等)都必須在工作內存中進行,而不能直接讀寫主內存中的變量(volatile變量仍然有工作內存的拷貝,但是由于它特殊的操作順序性規定,所以看起來如同直接在主內存中讀寫訪問一般)。不同的線程之間也無法直接訪問對方工作內存中的變量,線程之間值的傳遞都需要通過主內存來完成。
??
從更底層的來說,主內存對應的是硬件的物理內存,工作內存對應的是寄存器和高速緩存。
?JVM在設計時候考慮到,如果JAVA線程每次讀取和寫入變量都直接操作主內存,對性能影響比較大,所以每條線程擁有各自的工作內存,工作內存中的變量是主內存中的一份拷貝,線程對變量的讀取和寫入,直接在工作內存中操作,而不能直接去操作主內存中的變量。但是這樣就會出現一個問題,當一個線程修改了自己工作內存中變量,對其他線程是不可見的,會導致線程不安全的問題。因為JMM制定了一套標準來保證開發者在編寫多線程程序的時候,能夠控制什么時候內存會被同步給其他線程。
內存交互操作
內存交互操作有8種,虛擬機實現必須保證每一個操作都是原子的,不可在分的(對于double和long類型的變量來說,load、store、read和write操作在某些平臺上允許例外)
- lock? ? ?(鎖定):作用于主內存的變量,把一個變量標識為線程獨占狀態
- unlock (解鎖):作用于主內存的變量,它把一個處于鎖定狀態的變量釋放出來,釋放后的變量才可以被其他線程鎖定
- read? ? (讀取):作用于主內存變量,它把一個變量的值從主內存傳輸到線程的工作內存中,以便隨后的load動作使用
- load? ? ?(載入):作用于工作內存的變量,它把read操作從主存中變量放入工作內存中
- use? ? ? (使用):作用于工作內存中的變量,它把工作內存中的變量傳輸給執行引擎,每當虛擬機遇到一個需要使用到變量的值,就會使用到這個指令
- assign? (賦值):作用于工作內存中的變量,它把一個從執行引擎中接受到的值放入工作內存的變量副本中
- store? ? (存儲):作用于主內存中的變量,它把一個從工作內存中一個變量的值傳送到主內存中,以便后續的write使用
- write (寫入):作用于主內存中的變量,它把store操作從工作內存中得到的變量的值放入主內存的變量中
JMM對這八種指令的使用,制定了如下規則:
- 不允許read和load、store和write操作之一單獨出現。即使用了read必須load,使用了store必須write
- 不允許線程丟棄他最近的assign操作,即工作變量的數據改變了之后,必須告知主存
- 不允許一個線程將沒有assign的數據從工作內存同步回主內存
- 一個新的變量必須在主內存中誕生,不允許工作內存直接使用一個未被初始化的變量。就是懟變量實施use、store操作之前,必須經過assign和load操作
- 一個變量同一時間只有一個線程能對其進行lock。多次lock后,必須執行相同次數的unlock才能解鎖
- 如果對一個變量進行lock操作,會清空所有工作內存中此變量的值,在執行引擎使用這個變量前,必須重新load或assign操作初始化變量的值
- 如果一個變量沒有被lock,就不能對其進行unlock操作。也不能unlock一個被其他線程鎖住的變量
- 對一個變量進行unlock操作之前,必須把此變量同步回主內存
畫圖舉例:
分析:
1、首先,主內存中,initFlag=false。
2、線程1經過 read、load,工作內存處,initFlag = false,use 之后, 執行引擎處 !initFlag = true,此時,卡在while處。
3、線程2同理,經過 read、load 之后,此時工作內存處,initFlag=false,經過 use (initFlag=true)、assign后,initFlag=true。
4、因為線程2處的cpu修改了 initFlag 的值,會 馬上回寫 到主內存中(經過 store、write兩步)。
5、線程1處的cpu 通過 總線嗅探機制 嗅探到變化,會將工作內存中的數據 失效(initFlag=false失效)
6、線程1會 重新 去主內存 read 最新的數據(此時,主內存中的數據 initFlag=true)。
7、那么,線程1在讀取最新的數據時,執行引擎處,!initFlag = false,結束循環,輸出 “success”。
? ?JMM對這八種操作規則和對volatile的一些特殊規則就能確定哪里操作是線程安全,哪些操作是線程不安全的了。但是這些規則實在復雜,很難在實踐中直接分析。所以一般我們也不會通過上述規則進行分析。更多的時候,使用java的happen-before規則來進行分析。
JMM三大特性
Java內存模型是圍繞著并發編程中原子性、可見性、有序性這三個特征來建立的,那我們依次看一下這三個特征:
原子性
什么是原子性:一個操作不能被打斷,要么全部執行完畢,要么不執行。在這點上有點類似于事務操作,要么全部執行成功,要么回退到執行該操作之前的狀態。
為什么會有原子性問題:因為CPU 有時間片的概念,會根據不同的調度算法進行線程調度。當一個線程獲得時間片之后開始執行,在時間片耗盡之后,就會失去 CPU 使用權。所以在多線程場景下,由于時間片在線程間輪換,就會發生原子性問題。
舉個例子:你覺得num++是原子性操作嗎?看起來它就一行代碼,然而反編譯之后可以看到num++在內存中操作也是分為了三步操作,那么多線程同時進來就可能在某個步驟被線程的隨機調度打斷而產生的一系列的問題。
a = true; ?//原子性
a = 5; ? ? //原子性
a = b; ? ? //非原子性,分兩步完成,第一步加載b的值,第二步將b賦值給a
a = b + 2; //非原子性,分三步完成
a ++; ? ? ?//非原子性,分三步完成:1、讀取a的值,2、計算a的值+1,3、賦值
?如何保證原子性:1、synchronized一定能保證原子性,因為被其修飾的某段代碼,只能由一個線程執行,所以一定可以保證原子操作。2、juc(java.util.concurrent包)中的lock包和atomic包,他們也可以解決原子性問題.
可見性
什么是可見性:一個線程對共享變量做了修改之后,其他的線程立即能夠看到(感知到)該變量的這種修改(變化)。
為什么會有可見性問題:根據JMM內存模型,可以看到主內存和線程工作內存之間存在時間差(延遲)問題。
由于線程對共享變量的操作都是線程拷貝到各自的工作內存進行操作后才寫回到主內存中的,這就可能存在一個線程A修改了共享變量 i 的值,還未寫回主內存時,另外一個線程B又對主內存中同一個共享變量 i 進行操作,但此時A線程工作內存中共享變量 i 對線程B來說并不可見,這種工作內存與主內存同步延遲現象就造成了可見性問題,另外指令重排以及編譯器優化也可能導致可見性問題,通過前面的分析,我們知道無論是編譯器優化還是處理器優化的重排現象,在多線程環境下,確實會導致程序輪序執行的問題,從而也就導致可見性問題。
如何保證可見性:volatile的特殊規則保證了volatile變量值修改后的新值立刻同步到主內存,每次使用volatile變量前立即從主內存中刷新,因此volatile保證了多線程之間的操作變量的可見性,而普通變量則不能保證這一點。
除了volatile關鍵字能實現可見性之外,還有synchronized,Lock,final也是可以的。
使用Lock接口的最常用的實現ReentrantLock(重入鎖)來實現可見性:當我們在方法的開始位置執行lock.lock()方法,這和synchronized開始位置(Monitor Enter)有相同的語義,即使用共享變量時會從主內存中刷新變量值到工作內存中(即從主內存中讀取最新值到線程私有的工作內存中),在方法的最后finally塊里執行lock.unlock()方法,和synchronized結束位置(Monitor Exit)有相同的語義,即會將工作內存中的變量值同步到主內存中去(即將線程私有的工作內存中的值寫入到主內存進行同步)。
final關鍵字的可見性是指:被final修飾的變量,在構造函數數一旦初始化完成,并且在構造函數中并沒有把“this”的引用傳遞出去(“this”引用逃逸是很危險的,其他的線程很可能通過該引用訪問到只“初始化一半”的對象),那么其他線程就可以看到final變量的值。
有序性
什么是有序性:代碼按順序執行
為什么會有有序性問題:Java語言規定JVM線程內部維持順序話語義,只要程序結果不受影響,那么執行的指令是可以優化的,可以和編寫的代碼順序不一致,這就是指令重排。指令重排可能發生在多個階段,例如Java源代碼編譯階段、內存系統重排序等。但是指令重排有一個原則: as-if-seiral:不管怎么重排序,單線程的程序執行結果不能夠被改變,編譯器、處理器等都得遵循這個規范和準則。
如何保證有序性:Java提供了兩個關鍵字volatile和synchronized來保證多線程之間操作的有序性,volatile關鍵字本身通過加入內存屏障來禁止指令的重排序,而synchronized關鍵字通過一個變量在同一時間只允許有一個線程對其進行加鎖的規則來實現。
指令重排問題
在執行程序時為了提高性能,編譯器和處理器常常會對指令做重排序。重排序分三種類型:
1、編譯器優化的重排序。編譯器在不改變單線程程序語義的前提下,可以重新安排語句的執行順序。
2、指令并行的重排序?,F代處理器采用了指令級并行技術(Instruction-Level Parallelism, ILP)來將多條指令重疊執行。如果不存在數據依賴性,處理器可以改變語句對應機器指令的執行順序。
3、內存系統的重排序。由于處理器使用緩存和讀/寫緩沖區,這使得加載和存儲操作看上去可能是在亂序執行。
從java源代碼到最終實際執行的指令序列,會分別經歷下面三種重排序:
上述的1屬于編譯器重排序,2和3屬于處理器重排序。這些重排序都可能會導致多線程程序出現內存可見性問題。對于編譯器,JMM的編譯器重排序規則會禁止特定類型的編譯器重排序(不是所有的編譯器重排序都要禁止)。
對于處理器重排序,JMM的處理器重排序規則會要求java編譯器在生成指令序列時,插入特定類型的內存屏障(memory barriers,intel稱之為memory fence)指令,通過內存屏障指令來禁止特定類型的處理器重排序(不是所有的處理器重排序都要禁止)。
例如:volatile的有序性是通過禁止指令重排來實現的。為了性能,在JMM中,在不影響正確語義的情況下,允許編譯器和處理器對指令序列進行重排序。而禁止指令重排底層是通過設置內存屏障來實現。
處理器重排序與內存屏障
現代的處理器(物理處理器即CPU)使用寫緩沖區來臨時保存向內存寫入的數據。寫緩沖區可以保證指令流水線持續運行,它可以避免由于處理器停頓下來等待向內存寫入數據而產生的延遲。同時,通過以批處理的方式刷新寫緩沖區,以及合并寫緩沖區中對同一內存地址的多次寫,可以減少對內存總線的占用。雖然寫緩沖區有這么多好處,但每個處理器上的寫緩沖區,僅僅對它所在的處理器可見。這個特性會對內存操作的執行順序產生重要的影響:處理器排序后對內存的讀/寫操作的執行順序,不一定與內存實際發生的讀/寫操作順序一致!
常見處理器允許的重排序類型的列表:
?上表單元格中的“N”表示處理器不允許兩個操作重排序,“Y”表示允許重排序。
從上表我們可以看出:
- 常見的處理器都允許Store-Load重排序;
- 常見的處理器都不允許對存在數據依賴的操作做重排序。sparc-TSO和x86擁有相對較強的處理器內存模型,它們僅允許對寫-讀操作做重排序(因為它們都使用了寫緩沖區)。
※注1:sparc-TSO是指以TSO(Total Store Order)內存模型運行時,sparc處理器的特性。
※注2:上表中的x86包括x64及AMD64。
※注3:由于ARM處理器的內存模型與PowerPC處理器的內存模型非常類似,本文將忽略它。
※注4:數據依賴性后文會專門說明。
?
為了保證內存可見性,java編譯器在生成指令序列的適當位置會插入內存屏障指令來禁止特定類型的處理器重排序。JMM內存屏障分為四類:
?StoreLoad Barriers是一個“全能型”的屏障,它同時具有其他三個屏障的效果?,F代的多處理器大都支持該屏障(其他類型的屏障不一定被所有處理器支持)。執行該屏障開銷會很昂貴,因為當前處理器通常要把寫緩沖區中的數據全部刷新到內存中(buffer fully flush)。
數據依賴性
如果兩個操作訪問同一個變量,且這兩個操作中有一個為寫操作,此時這兩個操作之間就存在數據依賴性。數據依賴分下列三種類型:
上面三種情況,只要重排序兩個操作的執行順序,程序的執行結果將會被改變。
前面提到過,編譯器和處理器可能會對操作做重排序。但是,編譯器和處理器在重排序時,會遵守數據依賴性,編譯器和處理器不會改變存在數據依賴關系的兩個操作的執行順序。
注意,這里所說的數據依賴性僅針對單個處理器中執行的指令序列和單個線程中執行的操作,不同處理器之間和不同線程之間的數據依賴性不被編譯器和處理器考慮。
?
as-if-serial語義?
as-if-serial語義的意思指:不管怎么重排序(編譯器和處理器為了提高并行度),(單線程)程序的執行結果不能被改變。編譯器,runtime 和處理器都必須遵守as-if-serial語義。
【例】
double pi = 3.14; //A double r = 1.0; //B double area = pi * r * r; //C如上圖所示,A和C之間存在數據依賴關系,同時B和C之間也存在數據依賴關系。因此在最終執行的指令序列中,C不能被重排序到A和B的前面(C排到A和B的前面,程序的結果將會被改變)。但A和B之間沒有數據依賴關系,編譯器和處理器可以重排序A和B之間的執行順序。下圖是該程序的兩種執行順序:
as-if-serial語義把單線程程序保護了起來,遵守as-if-serial語義的編譯器,runtime 和處理器共同為編寫單線程程序的程序員創建了一個幻覺:單線程程序是按程序的順序來執行的。as-if-serial語義使單線程程序員無需擔心重排序會干擾他們,也無需擔心內存可見性問題。
?
volatile
下面這段話摘自《深入理解Java虛擬機》:
“觀察加入volatile關鍵字和沒有加入volatile關鍵字時所生成的匯編代碼發現,加入volatile關鍵字時,會多出一個lock前綴指令”
lock前綴指令實際上相當于一個內存屏障(也成內存柵欄),內存屏障會提供3個功能:
1)它確保指令重排序時不會把其后面的指令排到內存屏障之前的位置,也不會把前面的指令排到內存屏障的后面;即在執行到內存屏障這句指令時,在它前面的操作已經全部完成;
2)它會強制將對緩存的修改操作立即寫入主存;
3)如果是寫操作,它會導致其他CPU中對應的緩存行無效。
1、保證有序性(禁止指令重排)
Load指令(讀屏障):將內存的數據拷貝到處理器緩存
Store指令(寫屏障):讓當前線程寫入緩存的數據可以被其他線程看見
volatile的內存屏障策略:
volatile寫之前插入StoreStore屏障;(規則a,防止重排)
volatile寫之后插入StoreLoad屏障;(規則c,保障可見性)
volatile讀之后插入LoadStore屏障;(規則b,防止重排)
volatile讀之后插入LoadLoad屏障;(規則b,防止重排)
LoadLoad Barriers
排隊,當第一個讀屏障指令讀取數據完畢之后,后一個讀屏障指令才能夠進行加載讀取
(禁止讀和讀的重排序)
StoreStore Barriers
當A寫屏障指令寫完之后且保證A的的寫入可以被其他處理器看見,再進行B的寫入操作
(禁止寫與寫的重排序)
LoadStore Barriers
前一個讀屏障指令讀取完畢后,后一個寫屏障指令才會進行寫入
(禁止讀和寫的重排序)
StoreLoad Barriers
全能屏障,同時具有前三個的類型的效果,但開銷較大。
先保證A的寫入會被其他處理器可見,才進行B的讀取指令
(禁止寫和讀的重排序)
volatile:在每一個寫的volatile前后插入寫屏障,讀的volatile前后插入讀屏障。
寫,在每一次寫入之前屏障拿到其他的線程修改的數據(因為可見性和重排序)。寫入后的屏障可以被其他線程拿到最新的值。
讀,在每一個讀之前屏障獲取某個變量的值的時候,這個值可以被其他線程也獲取到。讀取后的屏障可以在其他線程修改之前獲取到主內存變量的當前值
?
?
2、保證可見性
volatile很好的保證了變量的可見性,變量經過volatile修飾后,對此變量進行寫操作時,匯編指令中會有一個LOCK前綴指令,這個不需要過多了解,但是加了這個指令后,會引發兩件事情:
- 將當前處理器緩存行的數據寫回到系統內存
- 這個寫回內存的操作會使得在其他處理器緩存了該內存地址無效
volatile修飾的共享變量在執行寫操作后,會立即刷回到主存,以供其它線程讀取到最新的記錄。
3、不保證原子性
volatile只有寫操作是原子性的,也就是數據操作完成后會立刻刷新到主內存中。但是被volatile修飾的變量在讀的時候可能會被多個線程讀。也就是說int i = 1;i++;
A線程讀 i = 1同時B線程也讀了i = 1,然后自增完成刷新入主內存。i的值是2。
所以如果該變量是volatile修飾的,那可以完全保證此時取到的是最新信息。但在入棧和自增計算執行過程中,該變量有可能正在被其他線程修改,最后計算出來的結果照樣存在問題,因此volatile并不能保證非原子操作的原子性,僅在單次讀或者單次寫這樣的原子操作中,volatile能夠實現線程安全。
CAS
在多線程編程時,如果想保證一段代碼具有原子性,通過會使用鎖來解決,而CAS是通過硬件指令來達到比較并交換的過程;簡單來說,CAS是操作系統層上的原子性操作。
CAS在java上的實現方式主要是JUC下的atomic原子類包
?
我們知道,Java是無法直接操作內存的,而C++可以,C++沒有虛擬機,因此在Java里面有native可以調用C++;
CAS操作原理
CAS包括三個值:
?V:內存地址;
?A:期望值;
?B:新值;
?如果這個內存地址V的值和期望值A相等,則將其賦值為B;?
例如:public final?int?getAndIncrement() 原子上增加一個當前值。
?這里的原子類方法用了do while,無限循環,其實就是一個標準的自旋鎖。
ABA問題
CAS只是比較和交換,失敗就返回false 。但是原子類里面的方法會用自旋鎖,無限循環,存在三個問題:
1、循環會耗時
2、一次性只能保證一個共享變量的原子性
3、ABA問題
什么是ABA問題
舉個例子
如何解決ABA問題
ABA解決思路還是使用樂觀鎖,版本號,時間戳的思想。對于樂觀鎖不記得了,可以回顧這篇文章
Mysql—鎖:全面理解_JagTom的博客-CSDN博客
在atomic包里面有個類 AtomicStampedReference<V>就是使用了版本號的實現方式
這里有個注意事項?
這句代碼是會有問題的:?
??
compareAndSet源碼,底層是用==進行判斷?,也就是我們在泛型使用包裝類的時候要注意,Integer類型的范圍是-128~127,超出范圍會在堆里面新建一個對象并不會復用對象。
?阿里巴巴規范手冊:
?AtomicStampedReference<V>源碼
public class AtomicStampedReference<V> {// 定義引用類型,包裝值和版本號;private static class Pair<T> {final T reference;final int stamp;private Pair(T reference, int stamp) {this.reference = reference;this.stamp = stamp;}static <T> Pair<T> of(T reference, int stamp) {return new Pair<T>(reference, stamp);}}private volatile Pair<V> pair;// 比較并交換public boolean compareAndSet(V expectedReference,V newReference,int expectedStamp,int newStamp) {Pair<V> current = pair;return// 先做一次校驗,如果在這里都已經不一致,則直接返回false,這里沒有加鎖,那么它可能會存在并發;// 可能會有兩個線程同時進來,判斷并且都成立,則兩個線程都會進入到:casPair方法;// Pair<V> current = pair; 多個線程進入到compareAndSet方法時,都已經保留了當前的pair值,那如果pair被其他線程修改,則另外一個線程去做cas的時候一定會返回false,所以這塊是通過這種方式來防止并發的;expectedReference == current.reference &&expectedStamp == current.stamp &&((newReference == current.reference &&newStamp == current.stamp) ||casPair(current, Pair.of(newReference, newStamp)));}private static final sun.misc.Unsafe UNSAFE = sun.misc.Unsafe.getUnsafe();private static final long pairOffset =objectFieldOffset(UNSAFE, "pair", AtomicStampedReference.class);private boolean casPair(Pair<V> cmp, Pair<V> val) {return UNSAFE.compareAndSwapObject(this, pairOffset, cmp, val);}static long objectFieldOffset(sun.misc.Unsafe UNSAFE,String field, Class<?> klazz) {try {return UNSAFE.objectFieldOffset(klazz.getDeclaredField(field));} catch (NoSuchFieldException e) {// Convert Exception to corresponding ErrorNoSuchFieldError error = new NoSuchFieldError(field);error.initCause(e);throw error;}} }各種鎖的理解
lock、synchronized默認都是可重入鎖,非公平鎖
公平鎖和非公平鎖
公平鎖:非常公平,不能夠插隊,必須先來后到!
非公平鎖:非常不公平,可以插隊(默認都是非公平)
public ReentrantLock { sync = new Nonfairsync(); //默認非公平}public ReentrantLock(boolean fair) { sync = fair ? new Fairsync() : new Nonfairsynco; //如果為true則設置為公平鎖 }可重入鎖
? ? ? ? 解釋一:可重入就是說某個線程已經獲得某個鎖,可以再次獲取鎖而不會出現死鎖。
? ? ? ? 這是可重入鎖的概念描述。
? ? ? ? 解釋二:可重入鎖又稱遞歸鎖,是指同一個線程在外層方法獲取鎖的時候,再進入該線程的內層方法會自動獲取鎖(前提是鎖對象得是同一個對象),不會因為之前已經獲取過鎖還沒有釋放而阻塞。
? ? ? ? 這是可重入鎖的一種表現方式,不代表說某段代碼中的鎖沒有發生嵌套,這個鎖就不是可重入鎖。
可重入鎖是某個線程已經獲得某個鎖,可以再次獲取鎖而不會出現死鎖。再次獲取鎖的時候會判斷當前線程是否是已經加鎖的線程,如果是對鎖的次數+1,釋放鎖的時候加了幾次鎖,就需要釋放幾次鎖。
? ? ? ? 代碼中的鎖的遞歸只是鎖的一種表現及證明形式,除了這種形式外,還有另一種表現形式。同一個線程在沒有釋放鎖的情況下多次調用一個加鎖方法,如果成功,則也說明是可重入鎖。
自旋鎖?
Java-concurrency/Java內存模型以及happens-before.md at master · three-body-zhangbeihai/Java-concurrency · GitHub
總結
以上是生活随笔為你收集整理的Java内存模型—JMM详解的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: mysql 回表查询优化_MySQL优化
- 下一篇: 兰州财经大学JAVA期末考什么_兰州财经