J.U.C JMM. pipeline.指令重排序,happen-before(續)

      前面已經介紹硬件平臺Cache Coherence問題和解決辦法,下面來看看Java虛擬機平臺的相關知識。硬件平臺處理器,高速緩存,主存之間的交互關係以下:java

Java內存模型(JMM)編程

       Java虛擬機規範中試圖定義一種Java內存模型(Java Memory Model, JMM)來屏蔽掉底層各類硬件和操做系統的內存訪問差別,以實現讓Java程序在各類平臺下都能達到一致的內存訪問效果。數組

  •   主內存和工做內存

      Java內存模型的主要目標是定義程序中各個變量的訪問規則,即在虛擬機中將變量存儲到內存和從內存中取出變量這些的底層細節。此處的變量與Java編程中所說的變量有所區別,它包括了實例字段,靜態字段和構成數組對象的元素,但不包括局部變量與方法參數,由於後者是線程私有的,不會被共享,天然也就不存在競爭問題。爲了得到較好的執行效能,Java內存模型並無限制執行引擎使用處理器的特定寄存器或者緩存來和主存進行交互,也沒有限制及時編譯器進行調整代碼執行順序之類的優化措施。緩存

      Java內存模型規定了全部的變量都存儲在主存中。每條線程還有本身的工做內存,線程的工做內存中保存了被該線程使用到的變量的主存副本的copy.線程對變量的全部操做(讀取,賦值等)都必須在工做內存中進行,而不能直接讀寫主存中的變量。不一樣的線程之間也沒法直接訪問對方工做內存中的變量,線程間變量值的傳遞均須要經過主存來完成,線程,主存,工做內存三者的交互關係以下:安全

  • 內存間交互操做

      關於主存和工做內存之間具體的交互協議,即一個變量如何從主存拷貝到工做內存,如何從工做內存同步到主存之類的實現細節,Java內存模型中定義了一下8種操做完成,虛擬機實現時必須保證下面說起的每一種操做都是原子的,不可再分的(對於double和long類型的變量來講,load, store, read和write操做在某些平臺下容許例外)併發

  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內存模型只要求上述兩個操做必須按順序執行,而沒有保證連續執行。也就是說,read和load操做之間,store和write操做之間是能夠插入其餘指令的,如主存中的變量a,b進行訪問時,一種可能出現的順序是read a, read b,l oad b, load a。除此以外,Java內存模型還規定了在執行上述8中基本操做時必須知足以下規則:app

  •      不容許read和load, store和write操做之一單獨出現, 既不容許一個變量從主存讀取了但工做內存不接受,或者從工做內存發起回寫了但主存不接受的狀況出現。
  •      不容許一個線程丟棄它最近的assign操做,即變量在工做內存中改變了以後必須把該變化同步到主存中。
  •      不容許一個線程無緣由的(沒有發生過任何assign操做)把數據從線程的工做內存同步回主存中。
  •      一個新的變量只能在主存中"誕生",不容許在工做內存中直接使用一個未被初始化(load或assign)的變量,換句話說,就是對一個變量實施use,store操做以前,必須先執行過了assign和load操做。
  •      一個變量在同一個時刻只容許被一條線程對其進行lock操做,但lock操做能夠被同一條線程重複執行屢次,屢次執行lock後,只有執行相同次數的unclock操做,變量纔會被解鎖。
  •      若是對一個變量執行lock操做,那將會清空工做內存中此變量的值,在執行引擎使用這個變量前,須要從新執行load或assign操做初始化變量的值。
  •      若是一個變量事先沒有被lock操做鎖定,那麼就不容許對它執行unlock操做,也不容許去unlock一個被其餘線程lock住的變量。
  •      對一個變量執行unlock操做以前,必須先把此變量同步回主存中(執行store, write操做)。

  

     這八種內存訪問操做以及上述規則限定,再加上稍後介紹的對volatile的一些特殊規定,就已經徹底肯定Java程序中哪些內存訪問操做在併發下是安全的。函數

 

Volatile關鍵字優化

     關鍵字volatile能夠說是Java虛擬機提供的最輕量級的同步機制。當一個變量定義爲volatile後,他將具有兩種特性this

     1:保證此變量對全部線程的可見性,這裏的"可見性"是指當一條線程修改了這個變量的值,新值對於其餘線程來講是當即得知的。而普通變量不能作到這一點,普通變量的值在線程間傳遞均須要經過主存來完成,例如:

線程A修改一個普通變量的值,而後向主存進行回寫,另一條線程B在線程A回寫完成以後再從主存進行讀取操做,新變量值纔會對線程B可見。

     因爲volatile變量只能保證可見性,在不符合一下條規則的運算場景中,咱們任然要經過加鎖(使用synchronized,reetrantlock或者java.util.comcurrent.atomic.*)來保證原子性。

  •    運算結果並不依賴變量的當前值(i++)。
  •    變量不須要和其餘的狀態變量共同參與不變性約束。

     2:使用volatile變量的第二個語義是禁止指令重排序優化。(具體指令重排序請見上篇)

     

    最後來看看Java內存模型中對volatile變量定義的特殊規則。假如T表示一個線程,V和W分別表示兩個volatile型變量,那麼在進行read, load, use, assgin,  store, write操做時須要知足以下規則:

  •     只有當線程T對變量V執行的前一個動做是load操做,線程T才能對變量V執行use操做動做;而且,只有當線程T對變量V執行的後一個動做是use的時候,線程T纔對變量V執行load操做。線程T變量V的use動做能夠認爲是和線程T對變量V的load. read動做相關聯,必須連續一塊兒出現(這條規則要求在工做內存中,每次使用V前都必須先從主存中刷新最新的值, 用於保證能看見其餘線程對變量V所作的修改後的值)。
  •     只有當線程T對變量V執行的前一動做是assign的時候,線程T才能對變量V執行store動做;而且,只有當線程T對變量V執行的後一個動做爲store的時候,線程T才能對變量V執行assign操做。線程T對變量V的assign操做能夠認爲是和線程T對變量V的store, write操做相關聯, 必須連續在一塊兒出現(這條規則要求在工做內存中,每次修改V後都必須馬上同步到主存中,用於保證其餘線程能夠看到本身對變量V所作的修改)。
  •     假定動做A是線程T對變量V實施的use或assgin操做, 假定動做F是和動做A相關聯的load或者store操做, 假定動做P是和動做F相應的對變量V的read, write操做;相似的,假定動做B是線程T對變量W實施的use或者assign操做,假定動做G是和動做B相關聯的Load和store操做, 假定動做Q是和動做G相應的對變量W的read和write操做。 若是A先於B, 那麼P先於Q(這條規則要求volatile修飾的變量不會被指令重排序優化,保證代碼的執行順序與程序的順序相同)

 

原子性,可見性,有序性

       Java內存模型是圍繞着在併發過程當中如何處理原子性,可見性和有序性這三個特徵創建的:

  •    原子性: 由Java內存模型來直接保證的原子性變量操做包括read, load,  use ,  assgin  ,  write,咱們大體能夠認爲基本數據類型的訪問讀寫是具有原子性的(long, double的非原子性協定)。若是應用場景須要一個範圍更大的原子性保證(常常會遇到),Java內存模型還提供了lock和unlock操做來知足這種需求,經管虛擬機未把lock和unlock操做直接開放給用戶使用,可是卻提供了更高層次的字節碼指令monitorenter和moniterexit來隱式的使用這兩個操做,這兩個字節碼指令反應到Java代碼中就是同步塊-synchronized關鍵字,所以在synchronized塊之間的操做也具有原子性。
  •    可見性: 可見性是當一個線程修改了共享變量的值, 其餘線程可以馬上得知這個修改。上面的volatile已經說明,Java內存模型是經過在變量修改後將新值同步回主存,在變量讀取前從主存刷新變量值這種依賴主存做爲傳遞媒介的方式來實現可見性,不管是普通變量仍是volatile變量都是如此,普通變量與volatile變量的區別是,volatile的特殊性保證了新值可以馬上同步到主存中,以及每次使用變量前馬上從主存刷新。除了volatile以外,Java還有兩個關鍵字可以實現可見性,即synchronized和final。 同步塊的可見性是由"對一個變量執行unlock操做以前,必須先把此變量同步回主存中(執行store ,write操做)"。final關鍵字的可見性是指,被final修飾的字段在構造器中一旦初始化完成,而且構造器沒有把this的引用傳遞出去(this引用逃逸是一件很危險的事情,其餘線程有可能經過這個引用訪問到"初始化了一半的"對象),那麼在其餘線程中就能看見final字段的值。
  •    有序性: Java內存模型的有序性在前面談volatile時說過,Java程序中自然的有序性能夠總結爲一句話,若是在本線程內觀察,全部的操做都是有序的;若是在一個線程中觀察另外一個線程,全部的操做都是無序的。前半句是指"線程內表現爲串行的語義",後半句是指"指令重排序"現象和"工做內存和主存同步延遲"現象。

 

happen-before

    Java內存模型下一些」自然的「先行發生關係,無序任何同步器協助就已經存在了,以下:

  1.     程序次序規則:      在一個線程內,按照程序代碼順序,書寫在前面的操做先行發生於書寫在後面的操做。
  2.     管程鎖定規則:      一個unlock操做先行發生於後面對同一個鎖的lock操做。
  3.     volatile變量規則 :對一個volatile變量的寫操做先行發生於後面對該變量的讀操做。
  4.     線程啓動規則:      Thread對象的start()方法先行發生於此線程的每個動做(即run()中的操做)。
  5.     線程終止規則:      線程中的全部操做都先行發生於對此線程的終止檢查。咱們能夠經過Thread.join()方法結束,Thread.isAlive()返回值等手段檢查到線程已經終止執行。
  6.     線程中斷規則:      對線程interrupt()方法的調用先行發生於對此線程被中斷線程的代碼檢測到中斷事件的發生,能夠經過Thread.interrupted(),Thread.isInterrupted()方法來檢查到是否發生中斷事件。
  7.     對象終結規則:      一個對象的初始化完成(構造函數執行完畢)先行發生於他的finalize()方法的開始。
  8.     傳遞性         :     若是操做A先行發生於操做B,操做B先行發生於C, 那麼就能夠獲得A先行發生於操做C。

 

線程安全:

      Java Concurrency in Practic: 當多個線程訪問同一類時,若是不要考慮這些線程在運行時環境下的調度和交替運行,而且不須要額外的同步及調用方代碼沒必要作其餘的協調,這個類的行爲任然是正確的,那麼這個類就是線程安全的。

      顯然只有資源競爭時纔會致使線程不安全,所以無狀態對象永遠是線程安全的。

      原子操做:多個線程執行一個操做時,其中任何一個線程要麼徹底執行完該操做,要麼沒有執行此操做的任何步驟,那麼這個操做就是原子的。

相關文章
相關標籤/搜索