從零開始了解多線程知識之開始篇目 — jvm&volatile

  1. CPU多核緩存存儲結構圖

    電腦存儲結構概念

    多CPU

    一個現代計算機通常由兩個或者多個CPU,如果要運行多個程序(進程)的話,假如只有 一個CPU的話,就意味着要經常進行進程上下文切換
    因為單CPU即便是多核的,也只是多個 處理器核心,其他設備都是共用的,所以多個進程就必然要經常進行進程上下文切換,這個代價是很高的。 
    

    CPU多核

    一個現代CPU除了處理器核心之外還包括寄存器、L1L2L3緩存這些存儲設備、浮點運算 單元、整數運算單元等一些輔助運算設備以及內部總線等。
    一個多核的CPU也就是一個CPU上 有多個處理器核心,這樣有什麼好處呢?比如說現在我們要在一台計算機上跑一個多線程的程序
    因為是一個進程里的線程,所以需要一些共享一些存儲變量,如果這台計算機都是單核單線程CPU的話,就意味着這個程序的不同線程需要經常在CPU之間的外部總線上通信,
    同時還 要處理不同CPU之間不同緩存導致數據不一致的問題,所以在這種場景下多核單CPU的架構就 能發揮很大的優勢,通信都在內部總線,共用同一個緩存。 
    

    CPU寄存器

     每個CPU都包含一系列的寄存器,它們是CPU內內存的基礎。CPU在寄存器上執行操作的速度遠大於在主存上執行的速度。
     這是因為CPU訪問寄存器的速度遠大於主存。
    

    CPU緩存

     即高速緩衝存儲器,是位於CPU與主內存間的一種容量較小但速度很高的存儲器。
     **由於CPU的速度遠高於主內存,CPU直接從內存中存取數據要等待一定時間周期,因此出現了CPU緩存**
     Cache中保存着CPU剛用過或循環使用的一部分數據,當CPU再次使用該部分數據時可從Cache中直接調用, 減少CPU的等待時間,提高了系統的效率。 
     CPU緩存包括 **一級Cache(L1 Cache) 二級Cache(L2 Cache) 三級Cache(L3 Cache) **
    

    內存

     一個計算機還包含一個主存。
     所有的CPU都可以訪問主存。主存通常比CPU中的緩存大得多。 
    

    CPU讀取存儲器數據過程

    CPU要取寄存器XX的值,只需要一步:直接讀取。 
    CPU要取L1 cache的某個值,需要1-3步(或者更多):把cache行鎖住,把某個數據拿 來,解鎖,如果沒鎖住就慢了。
    CPU要取L2 cache的某個值,先要到L1 cache里取,L1當中不存在,在L2里,L2開始加 鎖,加鎖以後,把L2里的數據複製到L1,再執行讀L1的過程,上面的3步,再解鎖。
    CPU取L3 cache的也是一樣,只不過先由L3複製到L2,從L2複製到L1,從L1到CPU。 
    CPU取內存則複雜:通知內存控制器佔用總線帶寬,通知內存加鎖,發起內存讀請求, 
    等待回應,回應數據保存到L3(如果沒有就到L2),再從L3/2到L1,再從L1到CPU,之後解除總線鎖定。 
    

    多線程環境下存在的問題

    緩存一致性問題

    在多處理器系統中,每個處理器都有自己的高速緩存,而它們又共享同一主內存 (MainMemory)。
    基於高速緩存的存儲交互很好地解決了處理器與內存的速度矛盾,但是 也引入了新的問題:**緩存一致性(CacheCoherence)**。
    當多個處理器的運算任務都涉及同一 塊主內存區域時,將可能導致各自的緩存數據不一致的情況,如果真的發生這種情況,
    那同步回到主內存時以誰的緩存數據為準呢?為了解決一致性的問題,需要各個處理器訪問緩存時都 遵循一些協議,
    

** 在讀寫時要根據協議來進行操作,這類協議有MSI、 MESI(IllinoisProtocol)、MOSI、Synapse、Firefly及DragonProtocol,等等**

  **指令重排序問題 **
  
   為了使得處理器內部的運算單元能盡量被充分利用,處理器可能會對輸入代碼進行亂序執行(Out-Of-Order Execution)優化,
   處理器會在計算之後將亂序執行的結果重組,保證該 結果與順序執行的結果是一致的,但並不保證程序中各個語句計算的先後順序與輸入代碼中的 順序一致。
   因此,如果存在一個計算任務依賴另一個計算任務的中間結果,那麼其順序性並不 能靠代碼的先後順序來保證。
   與處理器的亂序執行優化類似,Java虛擬機的即時編譯器中也有 類似的指令重排序(Instruction Reorder)優化

2.什麼是線程

現代操作系統在運行一個程序時,會為其創建一個進程。例如,啟動一個Java程序,操作系統就會創建一個Java進程。
現代操作系統調度CPU的最小單元是線程,也叫輕量級進程,在一個進程里可以創建多個線程,
**這些線程都擁有各自的計數器、堆棧和局部變量等屬性,並且能夠訪問共享的內存變量**。
處理器在這些線程上高速切換,讓使用者感覺到這些線程在同時執行。

線程的實現可以分為兩類:

1、用戶級線程(User-Level Thread)
2、內核線線程(Kernel-Level Thread)

在了解線程之前,需要知道系統存在兩種空間:**用戶空間和內核空間**
其中內核空間只能由內核代碼進行方訪問,用戶代碼無法直接訪問如果用戶代碼要訪問內核空間,
需要藉助內核空間提供的訪問接口
系統中用戶程序運行在用戶方式下,而系統調用運行在內核方式下。
在這兩種方式下所用的堆棧不一樣:用戶方式下用的是一般的堆棧,而內核方式下用的是固定大小的堆棧(一般為一個內存頁的大小)

比如一個4G內存的空間,可能只有3GB可以用於用戶應用程序。一個進程只能運行在用戶方式(usermode)或內核方式(kernelmode)下,
每個進程都有自己的3G用戶空間,它們共享1GB的內核空間。當一個進程從用戶空間進入內核空間時,它就不再有自己的進程空間了。
**這也就是為什麼我們經常說線程上下文切換會涉及到用戶態到內核態的切換原因所在**

用戶線程:

    **指不需要內核支持而在用戶程序中實現的線程,其不依賴於操作系統核心,應用進程利用線程庫提供創建、同步、調度和管理線程的函數來控制用戶線程**。
    另外,用戶線程是由應用進程利用線程庫創建和管理,不依賴於操作系統核心。不需要用戶態/核心態切換,速度快。
    操作系統內核不知道多線程的存在,因此一個線程阻塞將使得整個進程(包括它的所有線程)阻塞(可理解為串行化的)
    由於這裡的處理器時間片分配是以進程為基本單位,所以每個線程執行的時間相對減少。

內核線程:

    **線程的所有管理操作都是由操作系統內核完成的。內核保存線程的狀態和上下文信息**,
    當一個線程執行了引起阻塞的系統調用時,內核可以調度該進程的其他線程執行。
    在多處理器系統上,內核可以分派屬於同一進程的多個線程在多個處理器上運行,提高進程執行的並行度。
    由於需要內核完成線程的創建、調度和管理,所以和用戶級線程相比這些操作要慢得多,但是仍然比進程的創建和管理操作要快。
    大多數市場上的操作系統,如Windows,Linux等都支持內核級線程。

以下是用戶線程和內核線程的接口圖,用戶線程空間中,並沒有進程-線程對應關係表,但內核線程中有

Java線程與系統內核線程關係模型

Java線程
JVM中創建線程有2種方式

   1. new java.lang.Thread().start()
   2. 使用JNI將一個native thread attach到JVM中
   針對 new java.lang.Thread().start()這種方式,只有調用start()方法的時候,才會真正的在

JVM中去創建線程,主要的生命周期步驟有:

   1. 創建對應的JavaThread的instance
   2. 創建對應的OSThread的instance
   3. 創建實際的底層操作系統的native thread
   4. 準備相應的JVM狀態,比如ThreadLocal存儲空間分配等
   5. 底層的native thread開始運行,調用java.lang.Thread生成的Object的run()方法
   6. 當java.lang.Thread生成的Object的run()方法執行完畢返回後,或者拋出異常終止後,終止native thread
   7. 釋放JVM相關的thread的資源,清除對應的JavaThread和OSThread

針對JNI將一個native thread attach到JVM中,主要的步驟有:

   1. 通過JNI call AttachCurrentThread申請連接到執行的JVM實例
   2. JVM創建相應的JavaThread和OSThread對象
   3. 創建相應的java.lang.Thread的對象
   4. 一旦java.lang.Thread的Object創建之後,JNI就可以調用Java代碼了
   5. 當通過JNI call DetachCurrentThread之後,JNI就從JVM實例中斷開連接
   6. JVM清除相應的JavaThread, OSThread, java.lang.Thread對象

3.為什麼用到並發?並發會產生什麼問題

並發編程的本質其實就是利用多線程技術,在現代多核的CPU的背景下,催生了並發編程
的趨勢,通過並發編程的形式可以將多核CPU的計算能力發揮到極致,性能得到提升。除此之
外,面對複雜業務模型,並行程序會比串行程序更適應業務需求,而並發編程更能吻合這種業務拆分。

即使是單核處理器也支持多線程執行代碼,CPU通過給每個線程分配CPU時間片來實現
這個機制。時間片是CPU分配給各個線程的時間,因為時間片非常短,所以CPU通過不停地切
換線程執行,讓我們感覺多個線程是同時執行的,時間片一般是幾十毫秒(ms)。
並發不等於並行:並髮指的是多個任務交替進行,而並行則是指真正意義上的「同時進
行」。實際上,如果系統內只有一個CPU,而使用多線程時,那麼真實系統環境下不能並行,
只能通過切換時間片的方式交替進行,而成為並發執行任務。真正的並行也只能出現在擁有多個CPU的系統中。

並發的優點:

1. 充分利用多核CPU的計算能力;
2. 方便進行業務拆分,提升應用性能;

並發產生的問題:

高並發場景下,導致頻繁的上下文切換
臨界區線程安全問題,容易出現死鎖的,產生死鎖就會造成系統功能不可用

其它

CPU通過時間片分配算法來循環執行任務,當前任務執行一個時間片後會切換到下一個任務。
但是,在切換前會保存上一個任務的狀態,以便下次切換回這個任務時,可以再加載這個任務的狀態。
所以任務從保存到再加載的過程就是一次上下文切換。

什麼是JMM模型?

Java內存模型(Java Memory Model簡稱JMM)是一種抽象的概念,並不真實存在,它描
述的是一組規則或規範,通過這組規範定義了程序中各個變量(包括實例字段,靜態字段和構
成數組對象的元素)的訪問方式。JVM運行程序的實體是線程,而每個線程創建時JVM都會為
其創建一個工作內存(有些地方稱為棧空間),用於存儲線程私有的數據,而Java內存模型中規
定所有變量都存儲在主內存,主內存是共享內存區域,所有線程都可以訪問,但線程對變量的
操作(讀取賦值等)必須在工作內存中進行,首先要將變量從主內存拷貝的自己的工作內存空
間,然後對變量進行操作,操作完成後再將變量寫回主內存,不能直接操作主內存中的變量,
工作內存中存儲着主內存中的變量副本拷貝,前面說過,工作內存是每個線程的私有數據區
域,因此不同的線程間無法訪問對方的工作內存,線程間的通信(傳值)必須通過主內存來完
成。

JMM不同於JVM內存區域模型 (JVM是是實際存在的,JMM只是邏輯規則)

JMM與JVM內存區域的劃分是不同的概念層次,更恰當說JMM描述的是一組規則,通過
這組規則控制程序中各個變量在共享數據區域和私有數據區域的訪問方式,JMM是圍繞原子性,有序性、可見性展開。

JMM與Java內存區域唯一相似點,都存在共享數據區域和私有數據區域,在JMM中主內存屬於共享數據區域,
從某個程度上講應該包括了堆和方法區,而工作內存數據線程私有數據區域,
從某個程度上講則應該包括程序計數器、虛擬機棧以及本地方法棧。

線程,工作內存,主內存工作交互圖(基於JMM規範):

主內存

   主要存儲的是Java實例對象,所有線程創建的實例對象都存放在主內存中,不管該實例對
   象是成員變量還是方法中的本地變量(也稱局部變量),當然也包括了共享的類信息、常量、靜
   態變量。由於是共享數據區域,多條線程對同一個變量進行訪問可能會發生線程安全問題。

工作內存

   主要存儲當前方法的所有本地變量信息(工作內存中存儲着主內存中的變量副本拷貝),每
   個線程只能訪問自己的工作內存,即線程中的本地變量對其它線程是不可見的,就算是兩個線
   程執行的是同一段代碼,它們也會各自在自己的工作內存中創建屬於當前線程的本地變量,當
   然也包括了位元組碼行號指示器、相關Native方法的信息。注意由於工作內存是每個線程的私有
   數據,線程間無法相互訪問工作內存,因此存儲在工作內存的數據不存在線程安全問題。
   
   根據JVM虛擬機規範主內存與工作內存的數據存儲類型以及操作方式,對於一個實例對象中的成員方法而言,
   如果方法中包含本地變量是基本數據類型(boolean,byte,short,char,int,long,float,double),
   將直接存儲在工作內存的幀棧結構中,但倘若本地變量是引用類型,那麼該變量的引用會存儲在功能內存的幀棧中,
   而對象實例將存儲在主內存(共享數據區域,堆)中。但對於實例對象的成員變量,不管它是基本數據類型或者
   包裝類型(Integer、Double等)還是引用類型,都會被存儲到堆區。至於static變量以及類本身
   相關信息將會存儲在主內存中。需要注意的是,在主內存中的實例對象可以被多線程共享,倘
   若兩個線程同時調用了同一個對象的同一個方法,那麼兩條線程會將要操作的數據拷貝一份到
   自己的工作內存中,執行完成操作後才刷新到主內存

模型如下圖所示

JMM存在的必要性

在明白了Java內存區域劃分、硬件內存架構、Java多線程的實現原理與Java內存模型的具
體關係後,接着來談談Java內存模型存在的必要性。由於JVM運行程序的實體是線程,而每個
線程創建時JVM都會為其創建一個工作內存(有些地方稱為棧空間),用於存儲線程私有的數
據,線程與主內存中的變量操作必須通過工作內存間接完成,主要過程是將變量從主內存拷貝
的每個線程各自的工作內存空間,然後對變量進行操作,操作完成後再將變量寫回主內存,如
果存在兩個線程同時對一個主內存中的實例對象的變量進行操作就有可能誘發線程安全問題。

假設主內存中存在一個共享變量x,現在有A和B兩條線程分別對該變量x=1進行操作,
A/B線程各自的工作內存中存在共享變量副本x。假設現在A線程想要修改x的值為2,而B線程
卻想要讀取x的值,那麼B線程讀取到的值是A線程更新後的值2還是更新前的值1呢?答案是,不確定,
即B線程有可能讀取到A線程更新前的值1,也有可能讀取到A線程更新後的值2,
這是因為工作內存是每個線程私有的數據區域,而線程A變量x時,首先是將變量從主內存拷貝到A
線程的工作內存中,然後對變量進行操作,操作完成後再將變量x寫回主內,而對於B線程的也
是類似的,這樣就有可能造成主內存與工作內存間數據存在一致性問題,假如A線程修改完後
正在將數據寫回主內存,而B線程此時正在讀取主內存,即將x=1拷貝到自己的工作內存中,
這樣B線程讀取到的值就是x=1,但如果A線程已將x=2寫回主內存後,B線程才開始讀取的
話,那麼此時B線程讀取到的就是x=2,但到底是哪種情況先發生呢?

如圖 

以上關於主內存與工作內存之間的具體交互協議,即一個變量如何從主內存拷貝到工作內
存、如何從工作內存同步到主內存之間的實現細節,Java內存模型定義了以下八種操作來完
成。

JMM-同步八種操作介紹

(1)lock(鎖定):作用於主內存的變量,把一個變量標記為一條線程獨佔狀態
(2)unlock(解鎖):作用於主內存的變量,把一個處於鎖定狀態的變量釋放出來,釋放後的變量才可以被其他線程鎖定
(3)read(讀取):作用於主內存的變量,把一個變量值從主內存傳輸到線程的工作內存中,以便隨後的load動作使用
(4)load(載入):作用於工作內存的變量,它把read操作從主內存中得到的變量值放入工作內存的變量副本中
(5)use(使用):作用於工作內存的變量,把工作內存中的一個變量值傳遞給執行引擎
(6)assign(賦值):作用於工作內存的變量,它把一個從執行引擎接收到的值賦給工作內存的變量
(7)store(存儲):作用於工作內存的變量,把工作內存中的一個變量的值傳送到主內存中,以便隨後的write的操作
(8)write(寫入):作用於工作內存的變量,它把store操作從工作內存中的一個變量的值傳送到主內存的變量中

如果要把一個變量從主內存中複製到工作內存中,就需要按順序地執行read和load操作,
如果把變量從工作內存中同步到主內存中,就需要按順序地執行store和write操作。但Java內
存模型只要求上述操作必須按順序執行,而沒有保證必須是連續執行。

同步規則分析

   1)不允許一個線程無原因地(沒有發生過任何assign操作)把數據從工作內存同步回主內存中
   
   2)一個新的變量只能在主內存中誕生,不允許在工作內存中直接使用一個未被初始化(load或者assign)的變量。
   即就是對一個變量實施use和store操作之前,必須先自行assign和load操作。
   
   3)一個變量在同一時刻只允許一條線程對其進行lock操作,但lock操作可以被同一線程重複執行多次,多次執行lock後,
   只有執行相同次數的unlock操作,變量才會被解鎖。lock和unlock必須成對出現。
   
   4)如果對一個變量執行lock操作,將會清空工作內存中此變量的值,在執行引擎使用這個變
   量之前需要重新執行load或assign操作初始化變量的值。
   
   5)如果一個變量事先沒有被lock操作鎖定,則不允許對它執行unlock操作;也不允許去unlock一個被其他線程鎖定的變量。
   
   6)對一個變量執行unlock操作之前,必須先把此變量同步到主內存中(執行store和write操作) 

並發編程的可見性,原子性與有序性問題

原子性

   原子性指的是一個操作是不可中斷的,即使是在多線程環境下,一個操作一旦開始就不會
   被其他線程影響。
   在java中,對基本數據類型的變量的讀取和賦值操作是原子性操作有點要注意的是,對於
   32位系統的來說,long類型數據和double類型數據(對於基本數據類型,
   byte,short,int,float,boolean,char讀寫是原子操作),它們的讀寫並非原子性的,也就是說如
   果存在兩條線程同時對long類型或者double類型的數據進行讀寫是存在相互干擾的,因為對
   於32位虛擬機來說,每次原子讀寫是32位的,而long和double則是64位的存儲單元,這樣會
   導致一個線程在寫時,操作完前32位的原子操作後,輪到B線程讀取時,恰好只讀取到了後32
   位的數據,這樣可能會讀取到一個既非原值又不是線程修改值的變量,它可能是「半個變
   量」的數值,即64位數據被兩個線程分成了兩次讀取。但也不必太擔心,因為讀取到「半個變
   量」的情況比較少見,至少在目前的商用的虛擬機中,幾乎都把64位的數據的讀寫操作作為原
   子操作來執行,因此對於這個問題不必太在意,知道這麼回事即可。
   X=10;  //原子性(簡單的讀取、將數字賦值給變量)
   Y = x;  //變量之間的相互賦值,不是原子操作
   X++;  //對變量進行計算操作
   X = x+1;

可見性

   理解了指令重排現象後,可見性容易了,可見性指的是當一個線程修改了某個共享變量的
   值,其他線程是否能夠馬上得知這個修改的值。對於串行程序來說,可見性是不存在的,因為
   我們在任何一個操作中修改了某個變量的值,後續的操作中都能讀取這個變量值,並且是修改
   過的新值。
   但在多線程環境中可就不一定了,前面我們分析過,由於線程對共享變量的操作都是線程
   拷貝到各自的工作內存進行操作後才寫回到主內存中的,這就可能存在一個線程A修改了共享
   變量x的值,還未寫回主內存時,另外一個線程B又對主內存中同一個共享變量x進行操作,但
   此時A線程工作內存中共享變量x對線程B來說並不可見,這種工作內存與主內存同步延遲現象
   就造成了可見性問題,另外指令重排以及編譯器優化也可能導致可見性問題,通過前面的分
   析,我們知道無論是編譯器優化還是處理器優化的重排現象,在多線程環境下,確實會導致程
   序輪序執行的問題,從而也就導致可見性問題。

有序性

   有序性是指對於單線程的執行代碼,我們總是認為代碼的執行是按順序依次執行的,這樣
   的理解並沒有毛病,畢竟對於單線程而言確實如此,但對於多線程環境,則可能出現亂序現
   象,因為程序編譯成機器碼指令後可能會出現指令重排現象,重排後的指令與原指令的順序未
   必一致,要明白的是,在Java程序中,倘若在本線程內,所有操作都視為有序行為,如果是多
   線程環境下,一個線程中觀察另外一個線程,所有操作都是無序的,前半句指的是單線程內保
   證串行語義執行的一致性,後半句則指指令重排現象和工作內存與主內存同步延遲現象。

JMM如何解決原子性&可見性&有序性問題?

原子性問題

 除了JVM自身提供的對基本數據類型讀寫操作的原子性外,可以通過 synchronized和Lock實現原子性。
 因為synchronized和Lock能夠保證任一時刻只有一個線程訪問該代碼塊。

可見性問題

volatile關鍵字保證可見性。當一個共享變量被volatile修飾時,它會保證修改的值立即被
其他的線程看到,即修改的值立即更新到主存中,當其他線程需要讀取時,它會去內存中讀取
新值。synchronized和Lock也可以保證可見性,因為它們可以保證任一時刻只有一個線程能
訪問共享資源,並在其釋放鎖之前將修改的變量刷新到內存中

有序性問題

在Java裏面,可以通過volatile關鍵字來保證一定的「有序性」(具體原理在下一節講述
volatile關鍵字)。另外可以通過synchronized和Lock來保證有序性,很顯然,synchronized
和Lock保證每個時刻是有一個線程執行同步代碼,相當於是讓線程順序執行同步代碼,自然就
保證了有序性。

Java內存模型:
    每個線程都有自己的工作內存(類似於前面的高速緩存)。線程對變量的
    所有操作都必須在工作內存中進行,而不能直接對主存進行操作。並且每個線程不能訪問其他
    線程的工作內存。Java內存模型具備一些先天的「有序性」,即不需要通過任何手段就能夠得
    到保證的有序性,這個通常也稱為happens-before 原則。如果兩個操作的執行次序無法從
    happens-before原則推導出來,那麼它們就不能保證它們的有序性,虛擬機可以隨意地對它
    們進行重排序。

指令重排序:
    java語言規範規定JVM線程內部維持順序化語義。即只要程序的最終結果與
    它順序化情況的結果相等,那麼指令的執行順序可以與代碼順序不一致,此過程叫指令的重排
    序。指令重排序的意義是什麼?JVM能根據處理器特性(CPU多級緩存系統、多核處理器等)
    適當的對機器指令進行重排序,使機器指令能更符合CPU的執行特性,最大限度的發揮機器性
    能

下圖為從源碼到最終執行的指令序列示意圖

as-if-serial語義

    as-if-serial語義的意思是:不管怎麼重排序(編譯器和處理器為了提高並行度),(單線
    程)程序的執行結果不能被改變。編譯器、runtime和處理器都必須遵守as-if-serial語義。
    為了遵守as-if-serial語義,編譯器和處理器不會對存在數據依賴關係的操作做重排序,因
    為這種重排序會改變執行結果。但是,如果操作之間不存在數據依賴關係,這些操作就可能被
    編譯器和處理器重排序。

happens-before 原則

只靠sychronized和volatile關鍵字來保證原子性、可見性以及有序性,那麼編寫並發程序可能會顯得十分麻煩,
幸運的是,從JDK5開始,Java使用新的JSR-133內存模型,提供了happens-before原則來輔助保證程序執行的原子性、可見性以及有序性的問題,
它是判斷數據是否存在競爭、線程是否安全的依據,happens-before 原則內容如下

1. 程序順序原則,即在一個線程內必須保證語義串行性,也就是說按照代碼順序執行。

2. 鎖規則解鎖(unlock)操作必然發生在後續的同一個鎖的加鎖(lock)之前,也就是說,
    如果對於一個鎖解鎖後,再加鎖,那麼加鎖的動作必須在解鎖動作之後(同一個鎖)。
    
3. volatile規則 volatile變量的寫,先發生於讀,這保證了volatile變量的可見性,簡單
    的理解就是,volatile變量在每次被線程訪問時,都強迫從主內存中讀該變量的值,而當
    該變量發生變化時,又會強迫將最新的值刷新到主內存,任何時刻,不同的線程總是能
    夠看到該變量的最新值。
    
4. 線程啟動規則 線程的start()方法先於它的每一個動作,即如果線程A在執行線程B的
    start方法之前修改了共享變量的值,那麼當線程B執行start方法時,線程A對共享變量
    的修改對線程B可見
    
5. 傳遞性 A先於B ,B先於C 那麼A必然先於C

6. 線程終止規則 線程的所有操作先於線程的終結,Thread.join()方法的作用是等待當前
    執行的線程終止。假設在線程B終止之前,修改了共享變量,線程A從線程B的join方法
    成功返回後,線程B對共享變量的修改將對線程A可見。
    
7. 線程中斷規則 對線程 interrupt()方法的調用先行發生於被中斷線程的代碼檢測到中
    斷事件的發生,可以通過Thread.interrupted()方法檢測線程是否中斷。
    
8. 對象終結規則 對象的構造函數執行,結束先於finalize()方法

volatile內存語義
volatile是Java虛擬機提供的輕量級的同步機制。
volatile保證可見性與有序性,但是不能保證原子性,要保證原子性需要藉助synchronized、Lock鎖機制,同理也能保證有序性與可見性。
因為synchronized和Lock能夠保證任一時刻只有一個線程訪問該代碼塊。

volatile關鍵字有如下兩個作用

保證被volatile修飾的共享變量對所有線程總數可見的,也就是當一個線程修改了一個被volatile修飾共享變量的值,新值總是可以被其他線程立即得知。
禁止指令重排序優化。

volatile的可見性
關於volatile的可見性作用,我們必須意識到被volatile修飾的變量對所有線程總數立即可見的,會showtime,底層被編譯的時候會有lock信號
對volatile變量的所有寫操作總是能立刻反應到其他線程中
示例

    /**
     * 以下代碼先執行線程A,一直執行i++,然後執行線程B,更改initFlag的值為true,想要退出循環
     * 但是如果變量不加volatile或者不加鎖,由於線程A中的initFlag從第一次從主內存中load到線程A工作內存後
     * 一直使用的線程A的緩存數據,即便在線程B中更改了initFlag,但是並沒有showtime給線程A
     * 線程A使用的仍然是他緩存中的,並沒有去主內存中獲取,所以當前代碼要實現initFlag可見,
     * 可以加volatile關鍵字實現volatile寫(更改後一定會寫到主內存中並且會showtime),(保證可見性)
     * 或者加同步代碼塊synchronized
     *      加synchronized原因:看代碼第三版
     *
     */
    public class VolatileVisibilitySample {
        private boolean  initFlag = false;
        static Object object = new Object();
        public void refresh(){
            //普通寫操作,(主要改成volatile寫就可以)
            this.initFlag = true;
            String threadname = Thread.currentThread().getName();
            System.out.println("線程:"+threadname+":修改共享變量initFlag");
        }
        public void load(){
            String threadname = Thread.currentThread().getName();
            int i = 0;
    
    
            // 第一版 initFlag沒加volatile,後面的打印不會出現  (空跑會一直佔用CPU使用權,優先級別非常高)
            //while (!initFlag){ }
    
            // 第二版,加一個變量 initFlag沒加volatile,後面的打印不會出現,因為i和他沒關係
            //while (!initFlag){ i++;}
    
            // 第三版 加同步塊 initFlag沒加volatile,後面的打印會出現,
            // 存在同步塊,這裡可能引起阻塞,競爭可能導致上下文切換,線程的上下文切換會把線程的信息等數據回寫到內存的 任務狀態段 裏面
            // 所以可能重新去主內存load數據,能知道initFlag已經改變,得以更新線程A的內存副本
            while (!initFlag){
                synchronized (object){
                    i++;
                }
            }
            System.out.println("線程:"+threadname+"當前線程嗅探到initFlag的狀態的改變"+i);
        }
        public static void main(String[] args){
            VolatileVisibilitySample sample = new VolatileVisibilitySample();
            Thread threadA = new Thread(()->{ sample.refresh(); },"threadA");
            Thread threadB = new Thread(()->{ sample.load(); },"threadB");
            threadB.start();
            try {
                 Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            threadA.start();
        }
    }

volatile無法保證原子性

    public class VolatileVisibility {
        public  static  volatile int  i =0;
        public  static  void  increase(){i++;}
    }

    在並發場景下,i變量的任何改變都會立馬反應到其他線程中,但是如此存在多條線程同時調用increase()方法的話,
    就會出現線程安全問題,畢竟i++;操作並不具備原子性,該操作是先讀取值,然後寫回一個新值,相當於原來的值加上1,
    分兩步完成,如果第二個線程在第一個線程讀取舊值和寫回新值期間讀取i的域值,那麼第二個線程就會與第一個線程一起看到同一
    個值,並執行相同值的加1操作,這也就造成了線程安全失敗,因此對於increase方法必須使用synchronized修飾,以便保證線程安全,需
    要注意的是一旦使用synchronized修飾方法後,由於synchronized本身也具備與volatile相同的特性,即可見性,
    因此在這樣種情況下就完全可以省去volatile修飾變量

volatile禁止重排優化

volatile關鍵字另一個作用就是禁止指令重排優化,從而避免多線程環境下程序出現亂序
執行的現象,關於指令重排優化前面已詳細分析過,這裡主要簡單說明一下volatile是如何實
現禁止指令重排優化的。先了解一個概念,內存屏障(Memory Barrier)。
內存屏障,又稱內存柵欄,是一個CPU指令,它的作用有兩個,一是保證特定操作的執行順序,
二是保證某些變量的內存可見性(利用該特性實現volatile的內存可見性)。
由於編譯器和處理器都能執行指令重排優化。如果在指令間插入一條Memory Barrier則會告訴編譯器和CPU,
不管什麼指令都不能和這條Memory Barrier指令重排序,也就是說通過插入內存屏
障禁止在內存屏障前後的指令執行重排序優化。Memory Barrier的另外一個作用是強制刷出
各種CPU的緩存數據,因此任何CPU上的線程都能讀取到這些數據的最新版本。
總之,volatile變量正是通過內存屏障實現其在內存中的語義,即可見性和禁止重排優化。

下面看一個非常典型的禁止重排優化的例子DCL,如下

    /**
     * volatile保證指令重排(原理是插入了屏障)
     */
    public class Singleton {
    
        /**
         * 查看彙編指令
         * -XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly -Xcomp
         */
        private volatile static Singleton myinstance;
    
        public static Singleton getInstance() {
            if (myinstance == null) {
                synchronized (Singleton.class) {
                    if (myinstance == null) {
    
                        //多線程環境下可能會出現問題的地方
    
                        /** 對象創建過程,本質可以分文三步
                         * 1. 申請地址 address=allocate
                         * 2.地址上實例化對象 new Singleton()
                         * 3.第三步 myinstance=address
                         *
                         * 要加volatile關鍵字,為了阻止指令重排,原因:
                         *  其中這三步無法保證原子性,第二步和第三步可能存在指令重排
                         *  當很高的高並發請求下,如果不進行兩層判斷,
                         *  如果程序執行了第一步申請地址之後
                         *  如果第三步和第二步進行了指令重排,那麼會導致myinstance=address
                         *  但是這時候address是空的,在使用的時候就會報錯
                         *
                         *
                         *
                         */
                        myinstance = new Singleton();
                        //對象延遲初始化
                        //
                    }
                }
            }
            return myinstance;
        }
        public static void main(String[] args) {
            Singleton.getInstance();
        }
    
        /**
         * 如果在多線程環境下就可以出現線程安全問題。原因在於某一個線程執行到第一次檢測,讀
         * 取到的instance不為null時,instance的引用對象可能沒有完成初始化。
         * 因為instance = new Singleton();可以分為以下3步完成(偽代碼)
              memory = allocate();//1.分配對象內存空間
              instance(memory);//2.初始化對象
              instance = memory;//3.設置instance指向剛分配的內存地址,此時
              instance!=null
         * 由於步驟1和步驟2間可能會重排序,如下:
              memory=allocate();//1.分配對象內存空間
              instance=memory;//3.設置instance指向剛分配的內存地址,此時instance!
              =null,但是對象還沒有初始化完成!
              instance(memory);//2.初始化對象
         * 由於步驟2和步驟3不存在數據依賴關係,而且無論重排前還是重排後程序的執行結果在單
         * 線程中並沒有改變,因此這種重排優化是允許的。但是指令重排只會保證串行語義的執行的一
         * 致性(單線程),但並不會關心多線程間的語義一致性。所以當一條線程訪問instance不為null
         * 時,由於instance實例未必已初始化完成,也就造成了線程安全問題。那麼該如何解決呢,很
         * 簡單,我們使用volatile禁止instance變量被執行指令重排優化即可。
         * 
         //禁止指令重排優化
         private volatile static Singleton myinstance;
         */
    }

指令重排+讀寫屏障實例

/**
 * 指令重排,不允許使用volatile的話,手動插入屏障理解
 *
 * 從代碼上理解,正常的邏輯思維情況下下,可能打印的結果只有三種
 *  1,1 (當線程1執行了a=1,同時線程2執行了b=1的時候)
 *  1,0(當線程1執行了a=1,線程2還沒執行,y=b取了默認值)
 *  0,1(當線程2執行了b=1,線程1還沒執行,x=a取了默認值)
 *
 *  指令重排的結果
 *  0,0(a=1和x=b進行了指令重排,b=1和y=a進行了指令重排,xy都取了ab默認值)
 *
 */
public class VolatileReOrderSample {
    private static int x = 0, y = 0;
    private static int a = 0, b =0;
    //private volatile static int a = 0, b =0;
    static Object object = new Object();

    public static void main(String[] args) throws InterruptedException {
        int i = 0;

        for (;;){
            i++;
            x = 0; y = 0;
            a = 0; b = 0;
            Thread t1 = new Thread(new Runnable() {
                @Override
                public void run() {
                    //由於線程one先啟動,下面這句話讓它等一等線程two. 讀着可根據自己電腦的實際性能適當調整等待時間.
                    shortWait(10000);
                    a = 1; //是讀還是寫?store,volatile寫
                    //storeload ,讀寫屏障,不允許volatile寫與第二部volatile讀發生重排
                    //手動加內存屏障
                    //UnsafeInstance.reflectGetUnsafe().storeFence();

                    // 如果a,b使用volatile修飾,防止指令重排:這個操作 先讀volatile,然後寫普通變量b
                    x = b;
                    //分兩步進行,第一步先volatile讀,第二步再普通寫
                }
            });
            Thread t2 = new Thread(new Runnable() {
                @Override
                public void run() {
                    b = 1;
                    //UnsafeInstance.reflectGetUnsafe().storeFence();
                    y = a;
                }
            });
            t1.start();
            t2.start();
            t1.join();
            t2.join();
            String result = "第" + i + "次 (" + x + "," + y + ")";
            if(x == 0 && y == 0) {
                System.err.println(result);
                break;
            } else {
                System.out.println(result);
            }
        }

    }
    public static void shortWait(long interval){
        long start = System.nanoTime();
        long end;
        do{
            end = System.nanoTime();
        }while(start + interval >= end);
    }
}

volatile內存語義的實現

前面提到過重排序分為編譯器重排序和處理器重排序。為了實現volatile內存語義,JMM
會分別限制這兩種類型的重排序類型。
下面是JMM針對編譯器制定的volatile重排序規則表

    是否能重排序                    第二個操作
      第一個操作       普通讀/寫      volatile讀      volatile寫
      普通讀/寫                                        NO
      volatile讀       NO              NO             NO 
      volatile寫                       NO             NO

舉例來說,第三行最後一個單元格的意思是:

在程序中,當第一個操作為普通變量的讀或寫時,如果第二個操作為volatile寫,則編譯器不能重排序這兩個操作。
從上圖可以看出:

當第二個操作是volatile寫時,不管第一個操作是什麼,都不能重排序。
這個規則確保volatile寫之前的操作不會被編譯器重排序到volatile寫之後。

當第一個操作是volatile讀時,不管第二個操作是什麼,都不能重排序。
這個規則確保volatile讀之後的操作不會被編譯器重排序到volatile讀之前。

當第一個操作是volatile寫,第二個操作是volatile讀時,不能重排序。

為了實現volatile的內存語義,編譯器在生成位元組碼時,會在指令序列中插入內存屏障來禁止特定類型的處理器重排序。
對於編譯器來說,發現一個最優布置來最小化插入屏障的總數幾乎不可能。
為此,JMM採取保守策略。下面是基於保守策略的JMM內存屏障插入策略。

∙在每個volatile寫操作的前面插入一個StoreStore屏障。
∙在每個volatile寫操作的後面插入一個StoreLoad屏障。
∙在每個volatile讀操作的後面插入一個LoadLoad屏障。
∙在每個volatile讀操作的後面插入一個LoadStore屏障。

上述內存屏障插入策略非常保守,但它可以保證在任意處理器平台,任意的程序中都能得
到正確的volatile內存語義。
下面是保守策略下,volatile寫插入內存屏障後生成的指令序列示意圖

上圖中StoreStore屏障可以保證在volatile寫之前,其前面的所有普通寫操作已經對任意處理器可見了。
這是因為StoreStore屏障將保障上面所有的普通寫在volatile寫之前刷新到主內存。

這裡比較有意思的是,volatile寫後面的StoreLoad屏障。此屏障的作用是避免volatile寫與後面可能有的volatile讀/寫操作重排序。
因為編譯器常常無法準確判斷在一個volatile寫的後面是否需要插入一個StoreLoad屏障(比如,一個volatile寫之後方法立即return)。

為了保證能正確實現volatile的內存語義,JMM在採取了保守策略:

在每個volatile寫的後面,或者在每個volatile 讀的前面插入一個StoreLoad屏障。從整
體執行效率的角度考慮,JMM最終選擇了在每個 volatile寫的後面插入一個StoreLoad屏障。

因為volatile寫-讀內存語義的常見使用模式是:

一個寫線程寫volatile變量,多個讀線程讀同一個volatile變量。
當讀線程的數量大大超過寫線程時,選擇在volatile寫之後插入StoreLoad屏障將帶來可觀的執行效率的提升。
從這裡可以看到JMM 在實現上的一個特點:首先確保正確性,然後再去追求執行效率。

下圖是在保守策略下,volatile讀插入內存屏障後生成的指令序列示意圖

上圖中LoadLoad屏障用來禁止處理器把上面的volatile讀與下面的普通讀重排序。
LoadStore屏障用來禁止處理器把上面的volatile讀與下面的普通寫重排序。
上述volatile寫和volatile讀的內存屏障插入策略非常保守。在實際執行時,只要不
改變 volatile寫-讀的內存語義,編譯器可以根據具體情況省略不必要的屏障。
下面通過具體的示例

public class VolatileBarrierExample {
    int a;
    volatile int m1 = 1;
    volatile int m2 = 2;

    void readAndWrite() {
        int i = m1;   // 第一個volatile讀
        int j = m2;   // 第二個volatile讀

        a = i + j;    // 普通寫

        m1 = i + 1;   // 第一個volatile寫
        m2 = j * 2;   // 第二個 volatile寫
    }
}

針對readAndWrite()方法,編譯器在生成位元組碼時可以做如下的優化

注意,最後的StoreLoad屏障不能省略。因為第二個volatile寫之後,方法立即return。
此時編 譯器可能無法準確斷定後面是否會有volatile讀或寫,為了安全起見,編譯器通常會在這裡插 入一個StoreLoad屏障。
上面的優化針對任意處理器平台,由於不同的處理器有不同「鬆緊度」的處理器內 存模 型,
內存屏障的插入還可以根據具體的處理器內存模型繼續優化。以X86處理器為
例,圖3-21 中除最後的StoreLoad屏障外,其他的屏障都會被省略。
前面保守策略下的volatile讀和寫,在X86處理器平台可以優化成如下圖所示。前文提到過,X86處理器僅會對寫-讀操作做重排序。
X86不會對讀-讀、讀-寫和寫-寫操作做重排序,因此在X86處理器中會省略掉這3種操作類型對應的內存屏障。
在X86中,JMM僅需 在volatile寫後面插入一個StoreLoad屏障即可正確實現volatile寫-讀的內存
語義。這意味着在 X86處理器中,volatile寫的開銷比volatile讀的開銷會大很多(因為執行StoreLoad屏障開銷會比較大)

過多使用cas(compareandswap)和volatile導致的bus總線風暴
volatile 基於底層緩存一致協議

cpu –> 工作內存 –> bus總線(緩存一致性協議) —> 主內存
cpu1 –> 工作內存 –> bus總線(緩存一致性協議) —> 主內存

如果使用volatile特別多或者熱別多原子的cas,會導致工作內存見產生特別多無效工作內存變量,由於volatile在bus中無限showtime,
導致bus總線交互變得特別多,其他有意義的操作交互變得延遲
這時候和synchronized比較,還不如使用synchronized