JMM、Volatile、重排序、happen-before原則

2020-12-12 KOKO2019

這篇文章要梳理的概念會比較多,我在第一次接觸這些概念時理解了很久,反反覆覆,用了很長時間才弄明白個大概。我想在這篇文章把概念說清楚,每個概念本身都有很多外延的內容,外延的內容我會不斷學習後逐漸到文中。

JMM(Java Memory Model:java內存模型)

這是一個java技術規範,java的強大之一是它的多線程支持。java多線程執行期間是如何使用內存的呢?JMM就是這樣一個規範,它描述了多線程執行時的內存使用方式。

硬體層面,CPU的指令執行速度遠快於內存存取,為了緩解這種速度差,在CPU和內存之間,往往會有很多級速度比內存快的寄存器,存儲一些CPU頻繁訪問的變量,這比直接去頻繁訪問內存要快很多。

Java在多線程方面,也會充分利用以上物理特性,為每個線程構建私有的工作內存。JMM規範要求,線程對變量的讀寫,需要從主存拷貝變量副本到工作內存中,以提高執行性能,再在合適的時機同步回主存,以使其他線程可見。

盜用一張網絡圖片

上面提到,線程會在工作內存操作線程副本,合適時機同步回內存。這裡就會有一個同步問題,這是外延內容,以後補充。

這裡我們只需要了解:線程有自己的工作內存,對變量的讀寫是需要從主存拷貝進工作內存的。

如果是一個共享變量,多線程都在並發訪問,這時候會有一個問題:就是線程T1時序上先改了共享變量a,把值變成了a=1,原始值比如a=100

線程T2時序上在此後讀取a,T2讀取到的a值不一定等於1。注意這裡是不一定,不是必然。就是說T1雖然改了共享變量的值,其它線程比如T2不一定能看到這個值。因為T2讀取也是工作在自己的工作內存上,T1雖然做了修改,而這個也是發生在T1的工作內存上,只有T1把變量a的值同步回主存,T2再從主存拷貝a的值到自己的工作內存,才能保證T2能看到T1的修改。

這就是JMM規範下的一個變量在多線程情況下的可見性問題,下面要講解的Volatile關鍵字是一個解決可見性問題的一個關鍵字。

Volatile

volatile 是java語言當中的一個關鍵字。它用於聲明變量,聲明變量後,起到的作用是:保障變量在多線程環境下的可見性。

什麼意思呢?保障變量在多線程環境下的可見性。

在講述JMM時,我們最後說了一個問題,JMM規範下,一個線程修改了某個共享變量的值,另外一個線程即使在它之後執行,也不一定能看到這個變量被修改過後的值。我用以下代碼說明:

共享變量 boolean ready = false; //初始值false線程T1先執行以下代碼while(!ready){// wait}// ready, do something線程T2後執行以下代碼ready = true;//do other thing

看完以上代碼,我們可能以為線程T1啟動後,一直檢測ready的值,如果是false,就一直while循環,直到線程T2啟動,把ready的值改為true,線程T1才能退出循環。

其實真正執行以上代碼,你會發現,T1始終不能發現ready的值變更,會一直在while中死循環。這就是可見性問題,T1沒有發現這個變量變更了。

即使把以上共享變量換成數組,如

共享變量 boolean [] ready = new boolean[3]; //初始值false線程T1先執行以下代碼while(!ready[1]){// wait}// ready, do something線程T2後執行以下代碼ready[1] = true;//do other thing

它的效果也是一樣的。這時候,你給ready變量,加上volatile後,再試試

volatile boolean ready = false; 或

volatile boolean [] ready = new boolean[3];

你會發現,T2一執行,T1馬上跳出了循環。這就是volatile的作用,它保障了被修飾變量的可見性,T2一旦做過變更,T1就能看到。

那麼究竟volatile底層又是如何保障的呢,這屬於外延內容,我後續補充。

重排序

先說說什麼是重排序,看下面這段代碼。

int a = 1;char b = 『b』;byte c = 2;

這三行代碼很簡單,就是簡單的聲明了3個變量。你可能會認為,這三行代碼的執行順序就如寫代碼的順序一樣,按a b c的順序進行,實際上是不一定的。java語言是允許重排序的,也就是不按照abc的順序執行,可能是cba,bac都有可能。前提是不改變執行結果,數據之間不存在依賴。

如果

int a = 1;int b = a;

像上面這種b對a有數據依賴的,是不會被重排序的,執行順序必然是a在b之前。

重排序發生在以下幾個階段:

編譯器優化的重排序。編譯器在不改變單線程程序語義放入前提下,可以重新安排語句的執行順序。指令級並行的重排序。現代處理器採用了指令級並行技術來將多條指令重疊執行。如果不存在數據依賴性,處理器可以改變語句對應機器指令的執行順序。內存系統的重排序。由於處理器使用緩存和讀寫緩衝區,這使得加載和存儲操作看上去可能是在亂序執行。為什麼會重排序,上面這幾個階段裡大概提到了,提高並行效率,編譯器認為重排序後執行更優,指令級重排序並行效率更好等等。在單個線程的視角看來,重排序不存在任何問題,重排序不改變執行結果,如下例:

int a = 1;int b = 2;int c = a + b;

c因為對a和b有數據依賴,因此c不會被重排序,但是a 、b的執行可能被重排序。但在單個線程下,這種重排序不存在任何問題,不論先初始化a、還是先初始化b,c的值都是3。但是在多線程情況下,重排序就可能帶來問題,如下例:

線程T1執行:

a = 1; //共享變量 int ab = true; //共享變量 boolean b

線程T2執行:

if (b){int c = a;System.out.println(c);}

假如某個並發時刻,T2檢測到b變量已經是true值了,並且變量都對T2可見。c 賦值得到的一定是 1 嗎?

答案是不一定,原因就是重排序問題的存在,在多線程環境下,會造成問題。T1線程如果 a 和 b變量的賦值被重排序了,b先於 a發生,這個重排序對T1線程本身不存在什麼問題,之前我們已經討論過。但是在T2這個線程看來,這個執行就有問題了,因為在T2看來,如果沒有重排序,b值變為true之前,a已經被賦值1了。而重排序使得這個推斷變得不確定,b有可能先執行,a還沒來的及執行,此時線程T2已經看到b變更,然後去獲取a的值,自然不等於1。

happen-before原則

因為有以上重排序問題,會導致並發執行的問題,那麼有沒有方法解決呢?

happen-before原則,就是用來解決這個問題的一個原則說明,它告訴我們的開發者,你放心的寫並發代碼,但是你要遵循我告訴你的原則,你就能避免以上重排序導致的問題。

這個原則是什麼呢?

1、程序次序規則:在一個單獨的線程中,按照程序代碼的執行流順序,(時間上)先執行的操作happen—before(時間上)後執行的操作。2、管理鎖定規則:一個unlock操作happen—before後面(時間上的先後順序,下同)對同一個鎖的lock操作。3、volatile變量規則:對一個volatile變量的寫操作happen—before後面對該變量的讀操作。4、線程啟動規則:Thread對象的start()方法happen—before此線程的每一個動作。5、線程終止規則:線程的所有操作都happen—before對此線程的終止檢測,可以通過Thread.join()方法結束、Thread.isAlive()的返回值等手段檢測到線程已經終止執行。6、線程中斷規則:對線程interrupt()方法的調用happen—before發生於被中斷線程的代碼檢測到中斷時事件的發生。7、對象終結規則:一個對象的初始化完成(構造函數執行結束)happen—before它的finalize()方法的開始。8、傳遞性:如果操作A happen—before操作B,操作B happen—before操作C,那麼可以得出A happen—before操作C。

我們一條條看,

第一條

單線程情況下,happen-before原則告訴你,你放心的認為代碼寫在前面的就是先執行就ok了,不會有任何問題的。(當然實際並非如此,因為有指令重排序嘛,有的雖然寫在前面,但是未必先執行,但是單線程情況下,這並不會給實際造成任何問題,寫在前面的代碼造成的影響一定對後面的代碼可見)

happen-before 有一種記法,hb(a,b) ,表示a比b先行發生。

單線程情況下,寫在前面的代碼都比寫在後面的代碼先行發生

int a = 1;

int b= 2;

hb(a,b)

第二條

看如下代碼

線程T1:

a = 1;lock.unlock();b = ture;

線程T2:

if (b){lock.lock();int c = a;System.out.println(c);lock.unlock();}

此前在講重排序的時候說過這個問題,說c有可能讀取到的a值不一定是1。因為重排序,導致a的賦值語句可能沒執行。但是現在在

b賦值之前加了解鎖操作,線程T2在讀取到b值變更後,做了加鎖操作。這時候就是第二條原則生效的時候,它告訴我們,假如在時間上T1的lock.unlock()先執行了,T2 的lock.lock()後執行,那麼T1 unlock之前的所有變更,a = 1這個變更,T2是一定可見的,即T2 在 lock後,c拿到的值一定是 a 被賦值1的值。

因為 a = 1 和 lock.unlock() 有 hb 關係 hb(a=1 , lock.unlock() )

第二條原則 hb(unlock, lock), 而 hb(lock , c = a ),因此c在被賦值a時,a=1一定會先行發生。

第三條

volatile關鍵字修飾的變量的寫先行發生與對這個變量的讀,如下

線程T1:

a = 1;vv = 33;//volatileb = ture;

線程T2:

if (b){int ff = vv;// vv is volatileint c = a;System.out.println(c);}

與前面的鎖原則一樣,這次是volatile變量 寫happen-before讀。線程T2在讀取a變量前先讀取以下vv這個volatile變量。因為第三條原則的存在,只要T1在時間上執行了vv寫操作,T2在執行vv讀操作後,a=1的賦值一定可以被T2讀到。

第四條、第五條、第六條

線程T1 start方法,先行發生於T1即將做的所有操作。

如,在某個線程中啟動thread1

a = 1;thread1.start();

如上,a =1 先行發生 thread.start(),而第四條規則又說,start方法先行發生該線程即將做的所有操作,那麼a =1 ,也必將先行發生於 thread1 的任何操作。所以thread1啟動後,是一定可以讀取到a的值為1的

五、六條類似,線程終止前的所有操作先行發生於終止方法的返回。這就保障了一個線程結束後,其他線程一定能感知到線程所做的所有變更。

第七條

對象被垃圾回收調用finalize時,對象的構造一定已經先行發生。

第八條

傳遞性

至此,概念基本梳理完了。後續增加的外延有,分析java並發集合在實現時,為了符合happen-before的一些處理。

相關焦點

  • volatile如何防止指令重排序?原來使用了內存屏障
    2、數據依賴性上面的例子,你還會發現這樣一個特點,就算是發生了指令的重排序,但是最後的結果總是正確的。我們再舉一個例子:這種情況會發生指令重排序嗎?現在我們可以看到在多線程環境下如果發生了指令的重排序,會對結果造成影響。上面一開始提到過,volatile可以保證有序性,也就是可以防止指令重排序。那麼它是如何解決的呢?這就是內存屏障。因此我們從內存屏障講起。
  • 打工人,從 JMM 透析 volatile 與 synchronized 原理
    不過重排序並不意味著可以任意排序,它需要需要保證重排序後,不改變單線程內的語義,不能把對「秋香」說的話傳到「冬香」的耳朵裡,否則能任意排序的話,後果不堪設想,「時間管理大師」非你莫屬。CPU 重排序這裡的優化跟編譯器類似,目的都是通過打亂順序提高整體運行效率,這就是為了更快而執行的秘密武器。
  • 為什麼會有重排序?和 happens-before 有什麼關係
    就是因為在執行的過程中,發生了重排序。它可能是即時編譯器的重排序,可能是處理器的亂序執行,或者是內存系統的重排序。總之,在程序執行過程中,發生了重排序,然後得到的結果可能是 ( 0 , 0 ) 這種情況。為什麼會重排序看完上面,你可能會有疑問,為什麼會有重排序呢?
  • Java 多線程 —— 深入理解 volatile 的原理以及應用
    1 happens-before 2; 3 happens-before 42、基於volatile原則。2 happens-before 3;3、基於傳遞性原則。因為 1 happens-before 2,2 happens-before 3,3 happens-before 4。
  • Java程式設計師面試必備:Volatile全方位解析
    ,總線,MESI協議,嗅探技術) 4.Java內存模型(JMM) 5.並發編程的3個特性(原子性、可見性、有序性、happen-before、as-if-serial、指令重排) 6.volatile的底層原理(如何保證可見性,如何保證指令重排,內存屏障) 7.volatile的典型場景(狀態標誌,DCL單例模式) 8.volatile常見面試題
  • 可惜了,面試敗在了volatile關鍵字上,直擊痛點搞懂volatile
    這不,有位同學就來找我訴苦了,前兩次面試都挺順利的,到了三面竟然栽在了volatile關鍵字上。下面我們就來好好聊聊volatilevolatilevolatile 是一個類型修飾符。(實現可見性)禁止進行指令重排序。(實現有序性)volatile 只能保證對單次讀/寫的原子性。i++ 這種操作不能保證原子性。
  • 從底層原理深度剖析volatile關鍵字,徹底徵服面試官
    本篇文章從底層原理層面深度剖析volatile關鍵字是如何實現內存可見性的,同時引入了Java內存模型、指令重排序以及內存屏障等知識點作為原理分析的知識支撐。指令重排序對並發編程安全性有很大影響,所以提供了一些happens-before規則定義一些禁止編譯優化的場景。
  • 面試官:你知道happens-before規則嗎
    2、指令級並行重排序,它的意思是如果不存在數據依賴性,處理器可以改變語句對應機器碼指令順序,多條指令重疊執行。3、內存系統重排序,它的意思是現代處理器都有高速緩存和寫緩衝區,這樣看起來讀和寫操作像是亂序的。
  • 面試官最愛的 volatile 關鍵字,這些問題你都搞懂了沒?
    可見性即用volatile關鍵字修飾的成員變量表明該變量不存在工作線程的副本,線程每次直接都從主內存中讀取,每次讀取的都是最新的值,這也就保證了變量對其他線程的可見性。另外,使用volatile還能確保變量不能被重排序,保證了有序性。
  • 深入分析volatile是如何實現可見性和有序性的
    內存屏障可見性和有序性是基於各種內存屏障(禁止指令重排序)來實現的,先來看下有哪些內存屏障類型,以及可以解決那些因重排序引起的有序性問題。對有序性不太理解的同學可以先看下前這篇文章面試官:你知道happens-before規則嗎本文的重點來了
  • 面試官為什麼總是問happens-before規則,看完這篇文章你就懂了
    本篇文章從happens-before定義、用途以及具體規則三個方面對happens-before進行解讀,並通過源碼案例深入了解為什麼需要happens-before規則和什麼是指令重排序。為什麼需要happens-beforeJVM會對代碼進行編譯優化,會出現指令重排序情況,為了避免編譯優化對並發編程安全性的影響,需要happens-before規則定義一些禁止編譯優化的場景,保證並發編程的正確性。
  • 從零開始了解多線程知識之開始篇目——jvm&volatile
    Java內存模型具備一些先天的「有序性」,即不需要通過任何手段就能夠得到保證的有序性,這個通常也稱為happens-before 原則。如果兩個操作的執行次序無法從happens-before原則推導出來,那麼它們就不能保證它們的有序性,虛擬機可以隨意地對它們進行重排序。指令重排序:java語言規範規定JVM線程內部維持順序化語義。
  • Java中volatile特性概述
    禁止指令重排序:volatile可以防止指令重排序操作。volatile不保證原子性所謂的原子性是指在一次操作或者多次操作中,要麼所有的操作全部都得到了執行並且不會受到任何因素的幹擾而中 斷,要麼所有的操作都不執行。volatile不保證原子性。
  • Java研發技術——Volatile原理詳解
    重排序重排序是指編譯器和處理器為了優化程序性能在不影響程序執行結果的前提下而對指令序列進行重新排序的一種手段。上面三種情況,只要重排序兩個操作的執行順序,程序的執行結果就會被改變。前面提到過,編譯器和處理器可能會對操作做重排序。編譯器和處理器再重排序時,會遵守數據依賴性,編譯器和處理器不會改變存在數據依賴關係的兩個操作的順序。
  • java並發編程之volatile關鍵字
    >例如結果如果不設置stop變量為volatile,並不意味著程序一定不會結束因為這個stop變量的刷新操作也需要看cpu的處理方式但是加上volatile一定不會出現問題2、防止重排序但是由於作業系統可以對指令進行重排序,所以上面的過程也可能會變成如下過程:(1)分配內存空間。(2)將內存空間的地址賦值給對應的引用。
  • 深入synchronized和volatile底層原理
    為了遵守as-if-serial語義,編譯器和處理器不會對存在數據依賴關係的操作做重排序,因為這種重排序會改變執行結果。但是,如果操作之間不存在數據依賴關係,就會進行重排序。所以這種情況在單線程中不會出現什麼問題,因為編譯器可以分析其因關係。而對於多線程,在多個線程對一個變量進行訪問的時候,指令邏輯無法分辨因果關聯,不知道哪個應該先執行,哪個應該後執行,就可能出現亂序執行。
  • 你應該要理解的java並發關鍵字volatile
    提高java的並發編程,就不得不提volatile關鍵字,不管是在面試還是實際開發中 volatile都是一個應該掌握的技能。他的重要性不言而喻。因此也有必要學好。一、為什麼要用到volatile關鍵字?
  • 面試必問的volatile,你了解多少
    本來轉載自公眾號  佔小狼的博客作者佔小狼   來自 美團點評 基礎架構組前言Java中volatile這個熱門的關鍵字
  • 知名公司面試題:談談你對volatile關鍵字的理解
    對於一名java開發者,不管是在求職面試還是項目實際開發中,volatile都是一個需要掌握的知識點,是需要掌握好的。我們平時在閱讀源碼的過程中,時常會遇到volatile關鍵字,譬如Atomic類,通過源碼我們會發現volatile無處不在。為什麼要用到volatile關鍵字?