主要參考《深刻理解Java虛擬機》和《Java Concurrency in Practice》、以及各類官方非官方文檔的的總結。html
爲了解決處理器與內存之間的速度矛盾,引入了基於高速緩存的存儲交互。
但高速緩存的引入也帶來了新的問題:緩存一致性,即多處理器中,每一個處理器有各自的高速緩存,而他們又共享同一主內存。當多個處理器的運算任務額都涉及同一塊主存區域的時候,將可能致使各自的緩存數據不一致,若是真的發生這種狀況,那麼同步回到主存時以誰的緩存數據爲準呢?
爲了解決一致性的問題,須要各個處理器在訪問緩存時都遵循一些協議,在讀寫時要根據協議來進行操做。在本章中將會屢次提到「內存模型」一詞,能夠理解爲在特定的操做協議下,對特定的內存或高速緩存進行讀寫訪問時的過程抽象。不一樣的物理機器能夠擁有不一樣的內存模型。而Java虛擬機也擁有本身的內存模型,而且在這裏的內存訪問操做與硬件的訪問操做具備很高的可比性。java
Java虛擬機規範中試圖定義一種Java內存模型(Java Memory Model,JMM)來屏蔽掉各類硬件和操做系統的內存訪問差別,以實現讓Java程序在各類平臺下都能達到一致的內存訪問效果。在JDK1.5(實現了JSR-133)發佈後,Java內存模型已經成熟和完善起來了。程序員
Java內存模型的主要目標是定義程序中各個變量的訪問規則,即在虛擬機中將變量存儲到內存和從內存中取出變量這樣的底層細節。這裏的變量不包括局部變量和參數,由於其實線程私有的,不會被共享(但局部引用變量所指向的對象仍然是可共享的),天然不會存在競爭問題。
Java內存模型規定了全部的變量都存儲在主內存中(此處的主內存與介紹物理硬件時的主內存名字同樣,二者也能夠相互類比,但此處僅是虛擬機內存的一部分),每條線程還有本身的工做內存(可與高速緩存類比),線程的工做內存中保存了被該線程使用到的變量的主內存副本拷貝,線程對變量的全部操做(讀取賦值)都必須在工做內存中進行,而不能直接讀寫主內存中的變量(包括volatile變量也是這樣)。不一樣的線程之間也沒法直接訪問對方工做內存中的變量,線程間變量值的傳遞均須要經過主內存來完成。
編程
關於主內存和工做內存之間具體的交互協議:即一個變量如何從主內存拷貝到工做內存、如何從工做內存同步回主內存之類的實現細節。Java內存模型中定義瞭如下8種操做來完成,虛擬機實現時必須保證下面說起的每一種操做都是原子的、不可再分的(對於double、long類型的變量來講,load、store、read和write操做在某些平臺上容許有例外)。緩存
若是要把一個變量從主內存複製到工做內存,那麼就要順序地執行read和load操做,若是要把變量從工做內存同步回主內存,就要順序地執行store和write操做。注意:Java內存模型只要求上述兩個操做必須按順序執行,而沒有保證是連續執行。也就是說read和write之間是能夠插入其餘指令的,如對主內存的變量a、b進行訪問時,一種可能出現的順序是read a、read b、load b、load a。除此以外,Java內存模型還規定了在執行上述8中基本操做時所必須知足以下規則:安全
這八種內存訪問操做以及上述規則限定,再加上稍後介紹的對volatile的一些特殊規定,就已經徹底肯定了Java程序中哪些內存訪問操做在併發下是安全的。因爲這種定義至關嚴謹但確實比較繁瑣,實踐起來比較麻煩,因此後面會介紹這種定義的一個等效判斷原則————先行發生原則,用來肯定一個訪問在併發環境下是否安全。多線程
關鍵字能夠說是Java虛擬機提供的最輕量級的同步機制,但它並非很容易徹底被正確、完整地理解。以致於許多程序員都習慣不去使用它,遇到須要處理多線程數據競爭的時候一概使用synchronized來進行同步。
當將一個變量定義爲volatile以後,它將具有兩種特性,第一是保證此變量對全部線程的可見性,這裏的可見性是指當一條線程修改了這個變量的值,新值對於其餘線程來講是能夠當即得知的。而普通變量不能保證這一點,普通變量的值在線程間傳遞均須要經過主內存來完成,例如,線程A修改一個普通變量的值,而後向主內存進行回寫,另外一條線程B在線程A回寫完成了以後在從主內存進行讀取操做,新變量的值纔會對線程B可見。第二是禁止指令重排序優化,普通的變量僅僅會保證在該方法執行的過程當中全部依賴賦值的結果都能獲取到正確的結果,而不能保證變量賦值操做的順序與程序代碼中的執行順序一致。由於在一個線程的方法執行過程當中沒法感知這點,這就是Java內存模型當中所描述的「線程內表現爲串行」的語義(Within Thread As-If Serial Semantics)。併發
關於volatile可見性:volatile變量是對全部線程可見的,對volatile變量全部的寫操做都能馬上反映到其餘線程之中,換言之,volatile變量在各個線程中是一致的。但一致並不表明基於volatile變量的運算在併發下是安全的。volatile變量在各個線程的工做內存中不存在一致性的問題(在各個線程的工做內存中,volatile變量也能夠存在不一致的狀況,但在每次使用以前都要先刷新,執行引擎看不到不一致的狀況,因此能夠認爲不存在一致性的問題),但Java裏面的運算並不是是原子操做,致使volatile;變量在併發下同樣是不安全的。好比下例:app
/** * volatile變量自增測試運算 * @author xpc * @date 2018年12月16日下午8:40:02 */ public class VolatileTest { public static volatile int race=0; public static void increase() { race++; } public static final int THREADCOUNT=20; public static void main(String[] args) { Thread[] threads=new Thread[THREADCOUNT]; for(int i=0;i<THREADCOUNT;i++) { threads[i]=new Thread(()->{ for(int j=0;j<10000;j++) increase(); }); threads[i].start(); } while(Thread.activeCount()>1) { Thread.yield(); } System.out.println(race);//最後打印的結果是小於20*10000即200000的數 } }
其自增部分對應的字節碼爲函數
public static void increase(); descriptor: ()V flags: ACC_PUBLIC, ACC_STATIC Code: stack=2, locals=0, args_size=0 0: getstatic #13 // Field race:I 3: iconst_1 4: iadd 5: putstatic #13 // Field race:I 8: return LineNumberTable: line 11: 0 line 12: 8 LocalVariableTable: Start Length Slot Name Signature
之因此最後輸出的結果小於200000,而且每次運行程序輸出的結果都不同。問題就出如今自增運算race++上,反編譯後發現一個race++會產生4條字節碼指令(不包括return),從字節碼層面很容易分析出併發失敗的緣由:當getstatic指令把race的值取到操做棧頂時,volatile關鍵字保證了race的值在此時是正確的,但在執行iconst_一、iadd這些指令的時候,其餘線程可能已經把race的值加大了,而在操做棧頂的值就變成了過時的數據,因此putstatic指令執行後就可能把較小的race值同步回主內存之中。
這裏使用字節碼來分析併發問題仍然是不嚴謹的,由於即便編譯出來只有一條字節碼指令,也不意味這執行這條指令就是一個原子操做。一個字節碼指令在解釋執行時,解釋器將要運行許多行代碼才能實現它的語義。若是是編譯執行,一條字節碼指令可能轉化爲若干條本地機器碼指令。
因爲volatile只能保證可見性,在不符合如下兩條規則的運算場景中,仍然要經過加鎖(使用synchronized或者java.util.concurrent中的原子類)來保證原子性。同時知足如下兩條規則的運算場景才適合使用volatile去保證原子性
知足第一條但不知足第二條的一個例子:
volatile static int start = 3; volatile static int end = 6; //只有線程B修改變量的值,知足了第一條。儘管不知足運算結果不依賴變量的當前值,false||ture==ture 線程A執行以下代碼: while (start < end){ //do something } 線程B執行以下代碼: start+=3; end+=3;
適合使用volatile來控制併發的場景的例子,當shutdown()方法被調用時,能保證全部線程中執行的doWork()方法都當即中止下來。
volatile boolean shutdownRequested; public void shutdown(){ shutdownRequested=ture; } public void doWork(){ while(!shutdownRequested){ //do stuff } }
指令重排序干擾程序的併發執行的例子
Map configOptions; char[] configText; //此變量必須定義爲volatile volatile boolean initialized=false; //假設如下代碼在線程A中執行 //模擬讀取配置信息,當讀取完後將initialized設置爲true以通知其餘線程配置可用 configOptions=new HashMap(); configText=readConfigFile(fileName); processConfigOptions(configText,configOptions); initialized=true; //假設如下代碼在線程B中執行 //等待initialized爲true,表明線程A已經把配置信息處理化完成 while(!initialized){ sleep(); } //使用線程A初始化好的配置信息 doSomethingWithConfig();
在這個例子中,若是定義initialized變量時沒有使用volatile修飾,就可能會因爲指令重排序的優化,致使位於線程A中的最後一句的代碼initialized=true;被提早執行(雖然使用java代碼來做爲僞代碼,但所指的重排序優化是機器級的優化操做,提早執行是值這句話對應的彙編代碼被提早執行),這樣在線程B中使用配置信息的代碼就可能出現錯誤,而volatile關鍵字則能夠避免此類狀況的發生。
在對於jdk1.5以前的volatile關鍵詞而言,它只禁止了volatile變量指令之間的重排序,但在jdk1.5(JSR-133後)以後的volatile關鍵詞,不光禁止了volatile變量指令之間的指令重排序,還對volatile變量指令與非volatile變量指令之間的重排序增強了約束。這種增強後的約束具體原理是由內存屏障來實現的,這裏咱們不須要知道它的具體實現,只要知道它的實現效果是什麼。
JSR-133加強了volatile的內存語義後的效果就是:嚴格限制編譯器(在編譯器)和處理器(在運行期)對volatile變量與普通變量的重排序,確保volatile的寫-讀和監視器的釋放-獲取同樣,具備相同的內存語義(這也就是其達成的效果,正符合下面要講到的happens-before規則)。在《Java Concurrency In Practice》中也有一樣更完善的表述:volatile變量對可見性的影響比volatile變量自己(的可見性)更爲重要。當線程A首先寫入一個volatile變量而且線程B隨後讀取該變量時,在寫入volatile變量以前對A可見的全部變量的值(這些可見的值都是由後面的happens-before規則所肯定的),在B讀取了volatile變量後,對B也是可見的。所以,從內存可見性的角度來看,寫入volatile變量至關於退出同步代碼塊,而讀取volatile變量至關於進入同步代碼塊。
Under the new memory model, it is still true that volatile variables cannot be reordered with each other. The difference is that it is now no longer so easy to reorder normal field accesses around them. Writing to a volatile field has the same memory effect as a monitor release, and reading from a volatile field has the same memory effect as a monitor acquire.Effectively, the semantics of volatile have been strengthened substantially, almost to the level of synchronization. Each read or write of a volatile field acts like "half" a synchronization, for purposes of visibility.
所以,在JSR133後的也就是如今的新的內存模型下,由於有着happens-before規則(包含了volatile所保證的可見性與指令重排序),DCL單例模式就可以是有效的了(雖然仍是不推薦使用,由於有着更好的作法)
public class Singleton{ private volatile static Singleton instance;//在這裏主要是用到了禁止指令重排序的特性 public static Singleton getInstance() { if (instance == null) { synchronized (Singleton.class) { if (instance == null) instance = new Singleton(); } } return instance; } }
總結:在個人理解裏,volatile只保證了可見性,固然這個可見性是廣義的,並不單指volatile變量自己的可見性,還有上面提到的寫入時所能見到的全部其餘變量的值(這些可見的值都是由後面的happens-before規則所肯定的)的可見性。所謂的禁止指令重排序也只是爲了保證這個這個更廣義的可見性的擴广部分。固然也能夠說volatile變量具備狹義的可見性(僅其自己)和禁止指令重排序優化(實際上不是禁止而是嚴格約束,保證了擴广部分的可見性)。
此外加鎖機制既能夠保證可見性又能夠確保原子性,而volatile變量只能確保可見性。volatile是直接做用在變量上的,也就是說對於任何語句都是起效的。但鎖不一樣,鎖時直接做用在語句塊上的,訪問一樣的變量,這些加了鎖的語句塊在被訪問時是起效的,但那些沒被加了鎖的語句訪問時,由於鎖不在上面,天然就沒有同步的效果。因此一旦加了鎖的語句塊和未加鎖的語句塊併發訪問的是同一個變量的時候,就並不能保證可見性了。一個很好地例子就是下面的代碼:雖然set方法進行了同步,但get方法沒有進行同步。一個讀一個寫,而且二者之間沒有happens-before關係,因此是存在數據競爭狀況的,也就是說仍是可能會看到失效值。
public class MutableInteger{ private int value; public int get(){ return value;} public synchronized void set(int value){ this.value=value; } }
在某些狀況下,volatile的同步機制的性能確實要優於鎖(使用synchronized關鍵字或java.util.concurrent包裏面的鎖),可是因爲虛擬機對鎖實行的許多消除和優化,使得咱們很難量化地認爲volatile就會比synchronized快多少。 若是讓volatile本身與本身比較,那能夠肯定一個原則:volatile變量讀操做的性能消耗與普通變量幾乎沒有什麼差異,可是寫操做則可能會慢一些,由於它須要在本地代碼中插入許多內存屏障指令來保證處理器不發生亂序執行。 不過即使如此,大多數場景下volatile的總開銷仍然要比鎖低,咱們在volatile與鎖之中選擇的惟一依據僅僅是volatile的語義可否知足使用場景的需求。
對於非volatile變量,每當虛擬機遇到一個須要使用到變量值的字節碼指令的時候會執行use原子操做,當工做內存中不包含該變量值的時候就須要按順序執行read、load、use系列原子操做,若是工做內存中已經存在該變量的值了,那麼就不會再執行read、load從新從主內存中讀取該變量了(哪怕主內存中該變量的值已經被修改了);當虛擬機遇到一個給變量賦值的字節碼指令的時候回執行assign操做,對於assign操做,則必定會有按順序的assign、store、write。注意上面都是說的按順序但並非連續的,在指令重排序優化的狀況下中間可能穿插着其餘指令。
對於volatile變量,虛擬機遇到一樣的字節碼指令,read、load、use/assign、store、write這些原子操做必定是按順序連續發生的。這樣就保證了當即可見性。當即可見既指可以馬上見到其餘線程對該volatile變量的最新更新(讀操做),也能讓該線程對volatile變量的更新當即被其餘線程所見(寫操做)。
但注意當即可見性和原子性也是兩碼事(好比可見性裏面那個例子),這也是爲何上面會有關於volatile變量保證原子性使用場景的規則了,由於粗略地說,當只有是volatile變量只是被賦值如flag=true而且僅被做爲單一條件時如while(flag)時的時候才能保證原子性,由於這些操做對應的就只是按序連續的原子操做(並不包含自增或者比較之類的其餘操做),至關於一個整個大的原子操做。因此在Java併發編程實戰裏也舉了個使用volatile變量的典型用法:檢查某個狀態標記以判斷是否退出循環
使用volatile變量的例子:
volatile boolean asleep; ... while(!asleep){ countSomeSheep();
假定T表示一個線程,V和W分別表示兩個volatile變量,那麼在進行read, load, use, assign, store和write時須要知足如下三條規則:
java內存模型容許能夠將沒有被volatile修飾的64位的數據的讀寫操做(load、store、read、write)劃分爲兩次32位的操做來進行,這樣的話,多線程併發,就會存在線程可能讀取到「半個變量」的值,也就是非原子性協定。不過,這種狀況很是罕見,目前各平臺的商用虛擬機幾乎都選擇把64位的讀寫做爲原子操做來實現規範的,這也是Java內存模型鎖強烈建議的。
所以,雖然你知道了java內存模型對long和double型的變量定義了特殊規則,可是你也不用專門對這兩種類型的變量聲明爲volatile,由於上面說了,沒有虛擬機真的這樣實現了。
Java內存模型是圍繞着在併發過程當中如何處理原子性、可見性和有序性這3個特徵來創建的,咱們來看下哪些操做實現了這3個特性。
原子性(atomicity):
由Java內存模型來直接保證原子性變量操做包括read, load, assign, use, store和write。大體能夠認爲基本數據類型的訪問讀寫是具備原子性的。若是應用場景須要一個更大範圍的原子性保證,Java內存模型還提供了lock和unlock操做來知足需求,儘管虛擬機沒有把lock和unlock操做直接開放給用戶使用,可是卻提供了更高層次的字節碼指令monitorenter和monitorexit來隱式地使用這兩個操做,這兩個字節碼指令反映到Java代碼中就是同步塊——synchronized關鍵字,所以在synchronized塊之間的操做也具有原子性。
順便貼下《Java Concurrency In Practice》中對於原子性的定義:
假定有兩個操做A B,若是從執行A的線程來看,當另外一個線程執行B時,要麼將B所有執行完,要麼徹底不執行。那麼A和B對彼此來講是原子的。原子操做是指,對於訪問同一個狀態的全部操做(包括該操做自己)來講,這個操做是一個以原子方式執行的操做。
可見性(visibility):
可見性是指當一個線程修改了共享變量的值,其餘線程可以當即得知這個修改。Java內存模型是經過在變量修改後將新值同步回主內存、在變量讀取前從主內存刷新變量值這種依賴主內存做爲傳遞媒介的方式實現可見性的,不管是普通變量仍是volatile變量都是如此,普通變量與volatile變量的區別是,volatile的特殊規則保證了新值能當即同步到主內存,以及每次使用前當即從主內存刷新。所以,能夠說volatile保證了多線程操做時變量的可見性,而普通變量不能保證這一點。
除了volatile以外,Java還有兩個關鍵字能實現可見性,即synchronized和final。同步塊的可見性是由「對一個變量執行unlock操做以前,必須先把此變量同步回主內存中」這條規則得到的,而final關鍵字的可見性是指:被final修飾的字段在構造器中一旦初始化完成,而且構造器沒有把"this"的引用傳遞出去(this引用逃逸是一件很危險的事情,其餘線程有可能經過這個引用訪問到「初始化了一半」的對象),那在其餘線程中就能看見final字段的值。
有序性(Ordering):
Java程序自然的有序性能夠總結爲一句話:若是本線程內觀察,全部的操做都是有序的;若是在一個線程中觀察另外一個線程,全部的操做都是無序的。前半句是指「線程內表現爲串行的語義」,後半句是指「指令重排序」現象和「工做內存與主內存同步延遲」現象。
Java語言提供了volatile和synchronized兩個關鍵字來保證線程之間操做的有序性,volatile關鍵字自己就包含了禁止指令重排序的語義,而synchronized則是由「一個變量在同一時刻只容許一條線程對其進行lock操做」這條規則得到的,這條規則決定了持有同一個鎖的兩個同步塊只能串行地進入。
小結:綜上所述,synchronized關鍵字在須要這3種特性的時候均可以做爲其中一種的解決方案,看起來是比較萬能的,也的確大部分的併發控制操做都能使用synchronized來完成。這種萬能也形成了被程序員所濫用,越萬能也伴隨着越大的性能影響。
在Java語言中有一個「先行發生」(happens-before)的原則,有了這個原則咱們就能夠經過其中的規則來一攬子地解決併發環境下兩個操做之間可能存在的衝突的全部問題。
Java 內存模型是經過各類操做來定義,包括對變量的讀/寫操做,監視器的加鎖和釋放操做,以及線程啓動和合並操做。JMM爲程序中全部的操做定義了一個偏序關係,稱之爲Happens-Before。要想保證執行操做B的線程能看到操做A的結果
(不管A和B是否在同一個線程中執行),那麼在A和B之間必須知足Happens-Before關係。若是兩個操做之間缺少Happens-Before關係,那麼JVM能夠對它們任意地重排序。
上面的結果
一詞包括:修改了內存中共享變量的值、發送了消息、調用了方法等。
數據競爭:當一個變量被多個線程讀取而且至少一個線程寫入時,若是在讀操做寫操做之間沒有依照Happens-Before來排序,那麼就會產生數據競爭問題。在正確的同步的程序中不存在數據競爭,並會表現出串行一致性,這意味着程序中全部操做都會按照一種固定的和全局的順序執行。
Two accesses to (reads of or writes to) the same variable are said to be conflicting if at least one of the accesses is a write.When a program contains two conflicting accesses that are not ordered by a happens-before relationship, it is said to contain a data race.
下面會對前三條規則來進行舉例或者說明,從而理解先行發生這幾條規則的含義:
對於程序順序規則須要注意到的是要在同一個線程內,不是同一個線程之中的兩個操做是沒法直接經過程序次序規則來判斷出具備happens-before關係的。
private int value=0; public void setValue(){ this.value=value; } public int getValue(){ return value; }
上述代碼中顯示的一普通的getter/setter方法,假設存在線程A和線程B,線程A先(時間上的前後)調用了setValue(1),而後線程B調用了同一個對象的getValue(),那麼線程A收到的返回值是多少?
依次分析下先行發生原則的各項規則,因爲兩個方法分別由線程A和線程B調用,再也不一個線程中,因此程序次序規則在這裏不適用;沒有同步塊,天然也就不會發生lock和unlock操做,因此管程鎖定規則不適用;因爲value變量沒有被volatile 關鍵字修飾,因此volatile變量不適用;後面的線程啓動、終止、中斷規則和對象終結規則也和這裏徹底沒有關係。由於沒有一個適用的先行發生規則,因此最後一條的傳遞性也無從談起,所以咱們能夠斷定儘管線程A在操做時間上優於B,但沒法肯定線程B中的getValue()方法的返回結果,換句話說,這裏面的操做是線程不安全的。
那麼如何修復該問題呢?如下兩種方法都可:
另外有個例子,能夠更具體地說明先行發生與實際執行時間前後的關係:即實際執行時間的前後關係有時能夠根據某些規則推導出對應的先行發生關係,如第二條、第三條規則。但先行發生關係並不能肯定時間上的前後,除非在先行發生關係就是根據執行時間的前後來肯定的(就是前一句所說的)或者先行發生關係的後者操做必定會觀察前者操做的結果(或者成爲形成的影響)(固然有文章說cpu有着預測處理機制,因此即便觀察了也多是先執行的)。
//如下操做在同一個線程中執行 int i=1; int j=2;
上述代碼的賦值語句在同一個線程當中,根據程序次序規則,int i=1的操做先行發生於int j=2。可是先行發生的含義是後者操做可以觀察到前者操做的結果。可以是一種能力,後者操做不必定會去觀察,那麼若是後者操做沒有觀察的話(好比本例代碼中),即便先行發生關係的後者操做實際上先執行也沒有關係,仍是符合先行發生原則的。總而言之,先行發生關係描述的是一種後者操做可以見到前者操做結果的一種能力,而不是說後者操做必定要見到前者操做的結果(若是後者操做真的要觀察纔會必定觀察到前者操做的結果)。打個比喻就是你有傷人的能力,但不是說你必定會去傷人。
在此例中,int j=2徹底可能先被處理執行,但這並不影響先行發生原則的正確性,由於咱們在這條線程之中沒辦法感知到這點,若是換成int j=i固然就不同了。
上圖給出了當兩個線程使用同一個鎖(M)進行同步時,在它們之間的Happens-Before關係。在線程A內部的全部操做都按照它們在源碼程序中的前後順序來排序,在線程B內部的操做也是如此。因爲A釋放了鎖M,而且B隨後得到了鎖M,所以A在釋放鎖以前的操做,也就位於B請求鎖以後的全部操做以前。
若是這兩個線程是在不一樣的鎖上進行同步的,那麼就不能推斷它們之間的動做順序,由於在這兩個線程的操做之並不存在Happens-Before關係。
對於volatile變量的讀和寫而言,若是在實際執行時間上有寫在讀前的話(如線程A的assign在先於線程B的use執行,參看對volatile變量定義的特殊規則第三條),那麼就有寫在讀前的先行發生關係,這樣就保證了一切對於volatile變量寫操做可見的變量(即happens before volatile寫操做的其餘變量操做所形成的一切影響),對於後面的volatile變量讀操做也是可見的。若是換成可普通變量,即便是有時間上的寫在讀前,但如不是同一線程就沒有happens關係,這樣就不能保證可見性。
在實際編程中,一般讓咱們要判斷的是兩個對同一個變量讀寫訪問方法之間的是否具備happens-before關係,也就是說一系列操做與另外一系列操做以前的happens-before關係,但先行發生原則中一些規則只是肯定了兩個操做之間的先行發生關係,這個時候就有可能要用到傳遞性來判斷兩個操做系列之間的先行發生關係。
參考文章:
雙重檢查鎖定與延遲初始化