摘要:順序一致性內存模型有兩大特性一個線程中所有操作必須按照程序的順序執行。這里的同步包括對常用同步原語的正確使用通過以下程序說明與順序一致性兩種內存模型的對比順序一致性模型中所有操作完全按程序的順序串行執行。
java內存模型 java內存模型基礎 happen-before模型
JSR-133使用happen-before的概念來闡述操作之間的內存可見性。在JMM中,如果一個操作執行的結果需要對另一個操作可見,那么這兩個操作之間必須要存在happen-before關系。在這里兩個操作可以在一個線程之內,也可以在不同的線程之間。與程序員相關的happen-before規則如下:
程序順序一致性:一個線程中的每個操作,happen-before于該線程中的任意后續操作。(不要扣字,若兩操作沒有依賴關系,且變更操作順序不影響結果,此時順序可以變更。與程序一致性規則不沖突)
監視器鎖規則: 對一個鎖的解鎖,happen-before于隨后對這個鎖的加鎖。
volatile變量規則:對一個volatile域的寫操作,happen-before于任意后續對這個volatile域的讀。
傳遞性:如果A happen-before B且B happen-before C ,那么A happen-before C。
start()規則:如果線程A執行操作ThreadB.start(),那么A線程的ThreadB.start()操作happen-before于B中的任何操作。
join()規則:如果ThreadA 執行操作ThreadB.join()并成功返回,那么ThreadB中的任意操作happen-before于ThreadA從ThreadB.join()操作成功返回。
兩個操作間具有happens-before關系,并不意味著前一個操作必須要在后一個操作之前執行。happens-before僅僅要求前一個操作對后一個操作可見。
重排序重排序是指編譯器和處理器為了優化程序性能而對指令序列進行重新排序的一種手段。重排序得遵循以下原則。
數據相互信賴的兩個操作不能進行重排序
as-if-serial語言,不管怎么得排序(編譯器和處理器為了提高并行度),單線程程序執行的結果不能改變。
重排序對多線程的影響,代碼如下:
/** * 操作1 操作2 之間無依賴關系, 可以進行重排序 * 操作3 操作4 之間無依賴關系, 可以進行重排序 * Thread B 中并不一定能看到Thread A 中對共享變量的寫入。此時重排序操作破壞多線程語義 **/ class ReorderExample{ int a = 0; boolean flag = false; public void writer(){ //Thread A a = 1; //1 flag = true; //2 } public void reader(){ //Thread B if(flag){ //3 int i = a * a; //4 } } }順序一致性
順序一致性內存模型是一個理論參考模型,在設計的時候,處理器的內存模型和編程語言的內存模型都會以順序一致性內存模型作為參照。順序一致性內存模型有兩大特性:
一個線程中所有操作必須按照程序的順序執行。
(不管程序是否同步)所有線程都只能看到單一的操作執行順序,在順序一致性內存模型中,每個操作都必須原子執行且立即對所有線程可見。
JMM對正確同步的多線程程序的內存一致性做了如下保證
如果程序是正確同步的,程序的執行將具有順序一致性(Sequentially Consistent)--即程序的執行結果與該程序在順序一致性內存模型中執行結果相同。這里的同步包括對常用同步原語(Synchronized,volatile,final)的正確使用
通過以下程序說明JMM與順序一致性 兩種內存模型的對比
/** *順序一致性模型中,所有操作完全按程序的順序串行執行。而在JMM中,臨界區內的代碼 *可以重排序(但JMM不允許臨界區內的代碼“逸出”到臨界區之外,那樣會破壞監視器的語 *義)。JMM會在退出臨界區和進入臨界區這兩個關鍵時間點做一些特別處理,使得線程在這兩 *個時間點具有與順序一致性模型相同的內存視圖,雖然線程A在臨界 *區內做了重排序,但由于監視器互斥執行的特性,這里的線程B根本無法“觀察”到線程A在臨 *界區內的重排序。這種重排序既提高了執行效率,又沒有改變程序的執行結果。 * */ class SynchronizedExample { int a = 0; boolean flag = false; public synchronized void writer() { // 獲取鎖 a = 1; flag = true; } // 釋放鎖 public synchronized void reader() { // 獲取鎖 if (flag) { int i = a; ...... } // 釋放鎖 }
未同步程序在JMM中的執行時,整體上是無序的,其執行結果無法預知。
順序一致性模型保證單線程內的操作會按程序的順序執行,而JMM不保證單線程內的操作會按程序的順序執行(比如上面正確同步的多線程程序在臨界區內的重排序).
順序一致性模型保證所有線程只能看到一致的操作執行順序,而JMM不保證所有線程能看到一致的操作執行順序。
JMM不保證對64位的long型和double型變量的寫操作具有原子性,而順序一致性模型保證對所有的內存讀/寫操作都具有原子性。
volatile內存語義 volatile特性可見性:對一個volatile變量的讀,總是能看到(任意線程)對這個volatile變量最后的寫入。
原子性: 對任意單個volatile變量的讀/寫具有原子性,但類似于volatile++這種復合操作不具有原子性。
class VolatileFeaturesExample { volatile long vl = 0L; public void set(long l) { vl = l; } public void getAndIncrement () { //復合volatile讀寫,不具有線程安全 vl++; } public long get() { return vl; } }volatile 寫-讀建立的happen-before關系
示例代碼如下:
根據程序次序規則,1 happen-before 2, 3 happen-before 4.(疑問1:1與2,3與4兩操作沒有依賴,為何不能重排,若發生重排,結果有可能會發生變化)
根據volatile規則 2 happen-before 3
根據happen-before傳遞性規則,1 happen-before 4.
class VolatileExample { int a = 0; volatile boolean flag = false; public void writer() { a = 1; // 1 flag = true; // 2 } public void reader() { if (flag) { // 3 int i = a; // 4 ...... } }volatile 寫-讀內存原語
volatile寫操作,JMM會把線程對應的本地內存中的共享變量值刷新到主內存。
volatile讀操作,JMM會把該線程對應的本地內存置為無效。線程接下來將從主內存中讀取共享變量。
為了實現volatile內存語義,JMM分分別限制這兩種重排序類型,下圖JMM針對編譯器制定的volatile重排序規則表
這個重排序規則解釋了疑問1。實現:是通過編譯器生成字節碼時,插入內存屏障來達到這個限制,在此處不作展開,有興趣可以查閱相關資料
JSR-133增強volatile的內存原語在JSR-133之前的舊Java內存模型中,雖然不允許volatile變量之間重排序,但舊的Java內存模型允許volatile變量與普通變量重排序
因此,在舊的內存模型中,volatile的寫-讀沒有鎖的釋放-獲所具有的內存語義。為了提供一種比鎖更輕量級的線程之間通信的機制,JSR-133專家組決定增強volatile的內存語義:嚴格限制編譯器和處理器對volatile變量與普通變量的重排序,確保volatile的寫-讀和鎖的釋放-獲取具有相同的內存語義。從編譯器重排序規則和處理器內存屏障插入策略來看,只要volatile變量與普通變量之間的重排序可能會破壞volatile的內存語義,這種重排序就會被編譯器重排序規則和處理器內存屏障插入策略禁止。
當線程釋放鎖時,JMM會把該線程對應的本地內存中的共享變量刷新到主內存中。
當線程獲取鎖時,JMM會把該線程對應的本地內存置為無效。從而使得被監視器保護的臨界區代碼必須從主內存中讀取共享變量。
/** * */ class MonitorExample { int a = 0; public synchronized void writer() { // 1 a++; // 2 } // 3 public synchronized void reader() { // 4 int i = a; // 5 ...... } // 6 }
假設線程A執行writer()方法,隨后線程B執行reader()方法。根據happens-before規則,這個過程包含的happens-before關系可以分為3類。
根據程序次序規則,1 happens-before 2,2 happens-before 3;4 happens-before 5,5 happens-before 6。
根據監視器鎖規則,3 happens-before 4。
根據happens-before的傳遞性,2 happens-before 5。
鎖的釋放與獲取的內存語義線程釋放鎖時,JMM會把該線程對應的本地內存中的共享變量刷新到主內存中
線程獲取鎖時,JMM會把該線程對應的本地內存置為無效。從而使得被監視器保護的臨界區代碼必須從主內存中讀取共享變量
final 域內存語義final 域,編譯器與處理器要遵守兩個重排序規則
在構造函數內對一個final域的寫入,與隨后把這個被構造對像的引用賦值給一個引用變量,這兩個操作之間不能重排序
初次讀一個包含final域的對象引用,與隨后初次讀這個final域,這兩個操作之間不能重排序(有點擾,eg:obj,obj.j的關系)
下面的示例代碼,說明這兩個規則
/** * */ public class FinalExample{ int i; final int j; static FinalExample obj; static FinalExample(){ i = 1; j = 2; } public static void writer(){ obj = new FinalExample(); } public static void reader(){ FinalExample object = obj; int a = obj.i; int b = obj.j; } }寫final域重排序規則
MM禁止編譯器把final域的寫重排序到構造函數之外。
JMM編譯器會在final域的寫之后,構造函數return之前,插入一個StoreStore屏障。這個屏障禁止處理器把final域的寫重排序到構造函數之外
讀final域重排序規則一個線程中,初次讀對象引用與初次讀該對象包含的final域,JMM禁止處理器重排序這兩個操作
分析上面代碼示例 reader()方法包含3個操作。
初次讀引用變量obj。
初次讀引用變量obj指向對象的普通域j。
初次讀引用變量obj指向對象的final域
final域為引用類型/** *假設首先線程A執行writeOne方法,執行完后線程B執行writetwo()方法,執行完后線程執行reader()方法 *1是對final域的寫入,2是對這個final域引用的對象的成員域的寫入,3是把被構造的對象的引用賦值給某個引用變量。這里除了前面提到的1不能和3重排序外,2和3也不能重排序 */ public class FinalReferenceExample { final int[] intArray; static FinalReferenceExample obj; public FinalReferenceExample () { intArray = new int[1]; //1 intArray[0] = 1; //2 } public static void writerOne () { //線程A obj = new FinalReferenceExample (); //3 } public static void writerTwo () { //線程B obj.intArray[0] = 2; //4 } public static void reader () { //線程C if (obj != null) { //5 int temp1 = obj.intArray[0]; //6 } }
本例final域為一個引用類型,它引用一個int型的數組對象。對于引用類型,寫final域的重排序規則對編譯器和處理器增加了如下約束:在構造函數內對一個final引用的對象的成員域的寫入,與隨后在構造函數外把這個被構造對象的引用賦值給一個引用變量,這兩個操作之間不能重排序。
final引用不能從構造函數內溢出/** * 步驟2使得構造函數還未完成就對reader線程可見 **/ public class FinalReferenceEscapeExample { final int i; static FinalReferenceEscapeExample obj; public FinalReferenceEscapeExample () { i = 1; // 1寫final域 obj = this; // 2 this引用在此"逸出" } public static void writer() { new FinalReferenceEscapeExample (); } public static void reader() { if (obj != null) { // 3 int temp = obj.i; // 4 } }
結論:在構造函數返回前,被構造對象的引用不能為其他線程所見
雙重檢查鎖定與延遲初始化 java內存模型綜述文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/68857.html
摘要:因為管理人員是了解手下的人員以及自己負責的事情的。處理器優化和指令重排上面提到在在和主存之間增加緩存,在多線程場景下會存在緩存一致性問題。有沒有發現,緩存一致性問題其實就是可見性問題。 網上有很多關于Java內存模型的文章,在《深入理解Java虛擬機》和《Java并發編程的藝術》等書中也都有關于這個知識點的介紹。但是,很多人讀完之后還是搞不清楚,甚至有的人說自己更懵了。本文,就來整體的...
摘要:因為管理人員是了解手下的人員以及自己負責的事情的。處理器優化和指令重排上面提到在在和主存之間增加緩存,在多線程場景下會存在緩存一致性問題。有沒有發現,緩存一致性問題其實就是可見性問題。 網上有很多關于Java內存模型的文章,在《深入理解Java虛擬機》和《Java并發編程的藝術》等書中也都有關于這個知識點的介紹。但是,很多人讀完之后還是搞不清楚,甚至有的人說自己更懵了。本文,就來整體的...
摘要:編譯器,和處理器會共同確保單線程程序的執行結果與該程序在順序一致性模型中的執行結果相同。正確同步的多線程程序的執行將具有順序一致性程序的執行結果與該程序在順序一致性內存模型中的執行結果相同。 前情提要 深入理解Java內存模型(六)——final 處理器內存模型 順序一致性內存模型是一個理論參考模型,JMM和處理器內存模型在設計時通常會把順序一致性內存模型作為參照。JMM和處理器內...
摘要:內存模型即,簡稱,其規范了虛擬機與計算機內存時如何協同工作的,規定了一個線程如何和何時看到其他線程修改過的值,以及在必須時,如何同步訪問共享變量。內存模型要求調用棧和本地變量存放在線程棧上,對象存放在堆上。 Java內存模型即Java Memory Model,簡稱JMM,其規范了Java虛擬機與計算機內存時如何協同工作的,規定了一個線程如何和何時看到其他線程修改過的值,以及在必須時,...
摘要:作為一個程序員,不了解內存模型就不能寫出能夠充分利用內存的代碼。程序計數器是在電腦處理器中的一個寄存器,用來指示電腦下一步要運行的指令序列。在虛擬機中,本地方法棧和虛擬機棧是共用同一塊內存的,不做具體區分。 作為一個 Java 程序員,不了解 Java 內存模型就不能寫出能夠充分利用內存的代碼。本文通過對 Java 內存模型的介紹,讓讀者能夠了解 Java 的內存的分配情況,適合 Ja...
閱讀 3551·2021-11-08 13:15
閱讀 2107·2019-08-30 14:20
閱讀 1386·2019-08-28 18:08
閱讀 977·2019-08-28 17:51
閱讀 1484·2019-08-26 18:26
閱讀 2989·2019-08-26 13:56
閱讀 1484·2019-08-26 11:46
閱讀 2586·2019-08-23 14:22