java 类型不可视_jvm高级特性(5)(1)(原子性,可见性,有序性,volatile,概述)
簡介:
阿姆達爾定律(Amdahl):該定律通過系統中并行化與串行化的比重來描述多處理器系統能獲得的運算加速能力。
摩爾定律(Moore):該定律用于描述處理器晶體管數量與運行效率間的發展關系。
當價格不變時,集成電路上可容納的元器件的數目,約每隔18-24個月便會增加一倍,性能也將提升一倍。
Moore
系統中對某一部件采用更快執行方式所能獲得的系統性能改進程度,取決于這種執行方式被使用的頻率,或所占總執行時間的比例。
阿姆達爾定律實際上定義了采取增強(加速)某部分功能處理的措施后可獲得的性能改進或執行時間的加速比。
Amdahl
并發處理使得 Amdahl 定律代替摩爾定律成為計算機性能發展源動力的根本原因,也是人類“壓榨”計算機運算能力的最有力武器。
多任務處理幾乎是現代計算機操作系統的一項必備功能
由于計算機的運算速度與它的存儲和通信子系統速度差距太大,大量時間花費在磁盤IO等,所以才去多任務充分利用計算機處理器能力。
1. 硬件的效率與一致性
“讓計算機并發執行若干個運算任務”與“更充分地利用計算機處理器的效能”之間的因果關系,兩者之前關系并非簡單,
因為絕大多數的運算任務不可能只靠處理器“計算”就能完成,處理器至少要與內存交互,此IO操作很難消除。
由于計算機的存儲設備與處理器的運算速率有幾個數量級的差距,
所以現代計算機必須引入一層讀寫速度盡可能接近處理器速度的高速緩存(cache) 來作為內存與處理器間的緩沖:
將運算需要使用到的數據復制到緩存中,讓運算能快速進行,當運算結束后再從緩沖同步回內存中,這樣處理器就無須等待緩慢的內存讀寫了。
1,1緩存一致性(Cache Coherence)
緩存的引入產生了一個新問題——緩存一致性:
在多處理器系統中,每個處理器都有自己的高速緩存,而它們又共享同一主內存,
如下圖所示
當多個處理器的運算任務都涉及到同一塊內存區域時,將可能導致各自的緩存數據不一致,那同步到內存時以誰的數據為準呢?
解決方法:
需要各個處理器遵循一些協議,在讀寫時要根據協議來進行操作,這類協議有 MSI, MESI等。
1,2,亂序執行
為了使得處理器內部的運算單元能夠被盡量使用,處理器可能對輸入代碼進行亂序執行優化,
處理器會在計算之后將亂序執行的結果重組,保證該結果與順序執行的結果是一致的。
2. Java 內存模型
Java虛擬機規范試圖定義一種Java內存模型來屏蔽掉各種硬件和操作系統的內存訪問差異,以實現Java程序在各種平臺下都能達到一致的內存訪問效果。
2,1,Java內存模型的主要目標
定義程序中各個變量的訪問規則,即在虛擬機中將變量存儲到內存和從內存中讀取變量這樣的底層細節。
此處的變量不同于Java編程,不包括局部變量與方法參數,因為后者是線程私有的,不被共享,自然不會存在競爭問題。
2,2,Java內存模型規定了:所有的變量都存儲在主內存中。
每條線程還有自己的工作內存,線程的工作內存中保存了被該線程使用到的變量的主內存副本拷貝,
線程對變量的所有操作(讀取,賦值)都必須在工作內存中進行,而不能直接讀寫內存中的變量。
不同的線程之間也無法直接訪問對方工作內存中的變量。
2,3,線程、內存、工作內存三者關系
線程間變量值的傳遞均需要通過主內存來完成。
這里所講的主內存、工作內存與前面講解Java內存區域中的Java堆、棧、方法區等不是同一個層次劃分,兩者無任何關系。
即:內存模型和內存結構是兩個不同的概念。
3. 內存間交互操作
3,1,主內存與工作內存交互協議的8種操作:
即一個變量如何從主內存拷貝到工作內存,如何從工作內存同步回主內存的實現細節。
Java?內存模型中定義了以下8種操作(operations)來完成:
lock(鎖定) :作用于主內存的變量,它把一個變量標識為一條線程獨占的狀態。
unlock(解鎖):作用于主內存的變量,它把一個處于鎖定狀態的變量釋放出來,釋放后的變量才可以被其他線程鎖定。
read(讀取) :作用于主內存的變量,它把一個變量的值從主內存傳輸到線程的工作內存中,以便隨后的load 動作使用。
load(載入) :作用于工作內存的變量,它把 read 操作從主內存中得到的變量放入工作內存的變量副本中。
use(使用) :作用于工作內存的變量,它把工作內存中一個變量的值傳遞給執行引擎,每當虛擬機遇到一個需要使用到變量的值的字節碼指令時將會執行這個操作。
assign(賦值):作用于工作內存的變量,它把一個從執行引擎接收到的值賦給工作內存的變量,每當虛擬機遇到一個給變量賦值的字節碼指令時執行這個操作。
store(存儲) :作用于工作內存的變量,它把工作內存中一個變量的值傳送到主內存中,以便隨后的write操作使用。
write(寫入) :作用于主內存的變量,它把store操作從工作內存中得到的變量的值放入主內存的變量中。
3,2,執行8種操作需滿足的規則
如果要把一個變量從主內存復制到工作內存,那就要順序地執行 read 和 load 操作。
如果要把變量從工作內存同步到主內存,就要順序地操作 store 和 write 操作。
注意:
Java內存模型只要求上述兩個操作必須按照順序執行,而沒有保證是連續執行。
也就是說,read 和 load 之間、store 和 write 之間是可插入其他指令的。
如對內存中的變量a、b 進行訪問時,一種可能出現的順序是 :read a、read b、load b、load a 。
除此之外,Java內存模型還規定在執行上述8種操作需滿足的規則:
1,不允許read和load,store和write操作之一單獨出現,即不允許一個變量從主內存讀取了但工作內存不接受,或者從工作內存回寫了但主內存不接受的情況出現。
2,不允許一個線程丟棄它的最近的assign操作,即變量在工作內存中改變了之后必須把該變化同步回主內存。
3,不允許一個線程無原因地(沒有發生過任何assign操作)把數據從線程的工作內存同步回主內存中。
4,一個新變量只能在主內存中誕生,不允許在工作內存中直接使用一個未被初始化的變量,換句話說,對一個變量實施 use,store操作前,必須先執行過 assign 和 load 操作。
5,一個變量在同一個時刻只允許一個線程對其進行lock 操作,但lock操作可以被同一條線程重復執行多次,多次執行 lock后,只有執行相同次數的unlock 操作,變量才會被解鎖。
6,如果對一個變量執行lock 操作,那將會清空工作內存中此變量的值,在執行引擎使用這個變量前,需要重新執行 load 或 assign 操作初始化變量的值。
7,如果一個變量事先沒有被lock操作鎖定,那就不允許對它執行unlock操作,也不允許去 unlock 一個被其他線程鎖定住的變量;
8,對一個變量執行unlock 變量前,必須先把此變量同步回主內存中(執行store, write操作)。
PS:第6點有些疑問,為什么是載入和賦值,而不是載入和使用。個人理解為當前線程執行load前,是在其他線程執行assign完成后。
1,原子性:要么都執行要么都不執行。
在數據庫中,中間如果執行有問題可以回滾來保證所以操作是一個整體。。
而在并發中有兩個意思:
1,代表了原子性的操作(幾個步驟是一個原子)在線程執行過程中不會被中斷,是一個整體。2,原子性是拒絕多線程操作的,不論是多核還是單核,具有原子性的量,同一時刻只能有一個線程來對它進行操作!
ps:原子性:在其執行過程中,不允許其他并行線程對該變量進行讀取和寫入的操作。 如果發生競爭,則其他線程必須等待。
補充:
1,線程中斷:一段程序其實是很多條指令的集合,多線程的情況,每條線程被處理器分配一定執行時間來執行這些指令,所以執行并非連續的。
Java自帶的原子性
在Java中,對基本數據類型的變量的讀取和賦值操作是原子性操作。
賦值或者return。比如"a = 1;"和 "return a;"這樣的操作都具有原子性
非原子性操作:
類似"a += b" 或 “a++” 這樣的操作不具有原子性,經過三個步驟:
(1)取出a和b
(2)計算a+b
(3)將計算結果寫入內存
如果有兩個線程t1,t2在進行這樣的操作。t1在第二步做完之后還沒來得及把數據寫回內存就被線程調度器中斷了,
于是t2開始執行,t2執行完畢后t1又把沒有完成的第三步做完。這個時候就出現了錯誤,相當于t2的計算結果被無視掉了。
2,可見性:
可見性是指當多個線程訪問同一個變量時,一個線程修改了這個變量的值,其他線程能夠立即看得到修改的值。
如果線程t1與線程t2分別被安排在了不同的處理器上面,那么t1與t2對于變量A的修改時相互不可見,
如果t1給A賦值,然后t2又賦新值,那么t2的操作就將t1的操作覆蓋掉了,這樣會產生不可預料的結果。
所以,即使有些操作是原子性的,但是如果不具有可見性,那么多個處理器中備份的存在就會使原子性失去意義。
(ps:原子性應該只是讀和計算,而沒有寫入主內存)
來源《java編程思想》:原子性和可變性:
1?原子性
原子操作是不能被線程中斷機制中斷的操作,
一旦操作開始,則它一定在可能的切換到其他線程之前執行完畢。簡而言之就是不能被中斷的操作,如賦值或return。
1,1,對于讀寫除long和double之外的基本類型變量的簡單操作,可以保證它們的原子性來操作內存,
因為JVM將long和double這樣的64位的變量拆分成兩個分離的32位來操作,這樣很可能在一個讀取和寫入操作之間切換到其它線程,從而導致錯誤的結果。
1,2, 類似a+=2的操作不具備原子性,因為在JVM中這個操作需要三個步驟:
1)取出a2)計算a+2
3)將計算結果寫入內存
在上述步驟之間很可能線程調度器中斷,轉向另一個任務,這個任務可能修改這個域,造成結果錯誤,所以這個操作不是原子性的。
同樣a++也不具備原子性。(注:在C++中以上這兩種操作都是原子性的)
2?可視性
在多核處理器中,如果多個線程同時對一個變量進行操作,這些線程可能被分配到不同的CPU中運行。
由于編譯器會對代碼進行優化,當某個線程要對這個變量進行操作時,為了提高操作速度,
這個線程所在的CPU會從主存中復制這個變量到自己的緩存中,等操作完成后再存儲到主存中。
因此不同的線程對應的這個變量就有不同的狀態。(注:在單核處理器中也存在可視性問題)
變量的可視性:
假設有兩個線程T1和T2分別被安排到了兩個不同的CPU中(cpu1、cpu2),則T1和T2對變量a的修改互不可視,
如T1對a進行修改之后,只是對cpu1緩存中的a運行修改,沒有立即被寫入到主存中,
因此當線程T2再對a進行操作時,操作的并不是被線程T1修改后的新值。
此時線程T1和線程T2對于變量a是互不可視的。
不可視性產生的問題:
多個線程對某個變量的操作互不可視,可能造成某些操作被覆蓋,產生錯誤的結果。
如對于變量a,線程T1和線程T2都對其進行a++操作,T1操作a++后并沒有及時地將結果寫入到主存中去,而是繼續執行其它對a的操作,
當T2再執行a++操作后,它并沒有發現a的值已被線程T1修改,這樣就由于a的值沒有被及時更新而產生錯誤。
PS:
沒有原子性產生的問題:當前線程執行中斷。其他線程覆蓋執行。
不可視性產生的問題:(有原子性的情況)執行完變量的原子性的部分后,繼續執行對變量相關的其他操作。其他線程不知道變量的變化,覆蓋執行。
二?、?volatile與synchronized關鍵字在原子性和可視性中的應用
(1)當對long和double類型的變量用關鍵字volatile修飾時,就能獲得簡單操作(賦值和(return)的原子性。
但除對long和double簡單類型的簡單操作外,volatile并不能提供原子性,即使對一個變量用volatile修飾,對這個變量的操作也不是原子性的。
(2)volatile與可視性:
volatile賦予變量可視性,它修飾的變量每次被線程訪問時都要從主存中重新讀取該變量的值,
并且當變量的值發生變化時會強迫線程刷新到主存中去,這樣在任何時刻,從不同的線程看某一變量都是相同的值,從而保證了變量的可視性。
使用場合:
如果多個線程同時訪問某個變量,那個這個變量就應該用volatile修飾,
如果這個變量已在由synchronized修飾的方法或代碼塊中,則不必再用volatile修飾。(注:優先選擇使用synchronized關鍵字,因為這是最安全的方式)
原理:
為了獲得最佳速度,允許線程保存共享成員變量的私有拷貝,而且只當線程進入或者離開同步代碼塊時才與共享成員變量的原始值對比。
這樣當多個線程同時與某個對象交互時,就必須要注意到要讓線程及時的得到共享成員變量的變化。
而volatile關鍵字就是提示JVM:對于這個成員變量不能保存它的私有拷貝,而應直接與共享成員變量交互。
因此當要訪問的變量已在synchronized代碼塊中(因為線程進入或離開同步代碼塊時會更新共享變量的值),或者為常量時,不必再使用volatile。
由于使用volatile屏蔽掉了JVM中必要的代碼優化,所以在效率上比較低。
(3)synchronized關鍵字與操作原子性:
synchronized為一段操作或內存加鎖,具有互斥性。
當線程要操作被synchronized修飾的方法或代碼塊時,必須先獲得鎖。
但是同一時刻只能有一個線程持有同一把鎖(對象監示器),所以只允許一個線程對被synchronized修飾的方法或代碼塊進行操作。
synchronized關鍵字可以看作是解決多線程原子性操作的方法,因為它拒絕同一時刻多個線程對同一資源的訪問。
volatile的兩大特殊規則
1. 對于 volatile 型變量的特殊規則
1,1,volatile的特性 —– 可見性
可見性:當一條線程修改了這個變量的值,新值對于其他線程來說是可以立即得知的。
而普通變量做不到這一點,其在線程間傳遞需要通過主內存來完成。
例如,線程A修改一個普通變量的值,然后向主內存進行回寫,另外一條線程B在線程A回寫完成之后再從主內存進行讀取操作,新變量值才會對線程B可見。
“可見性”的誤解:
錯誤認為:volatile變量對所有線程都是可見的,對volatile變量所有的寫操作都能立刻反應到其他線程中,
實際:volatile變量在各個線程中是一致的,但在并發下是不安全的。
并發下volatile的不安全性示例
volatile變量在各個線程的工作內存中不存在一致性問題,但Java運算并非原子操作,導致?volatile變量的運算在并發下一樣是不安全的
public classVolatileTest {public static volatile int race = 0;public static voidincrease() {
race++;
}public static final int THREADS_COUNT = 20;public static voidmain(String[] args) {
Thread[] threads= newThread[THREADS_COUNT];for (int i = 0; i < threads.length; i++) {
threads[i]= new Thread(newRunnable() {
@Overridepublic voidrun() {for (int j = 0; j < 10000; j++) {
increase();
}
}
});
threads[i].start();
}//等待所有累計線程都ending
while(Thread.activeCount() > 1) {
Thread.yield();
}
System.out.println(race);
}
}
如果這段代碼能夠正確并發,最后輸出結果是200000。但是運行了3次,得出的結果都是小于200000的一個數字。
并發失敗原因:
問題在于自增運算race++?,increase()?方法在Class文件中是由4條字節碼指令構成:
從字節碼層面上分析并發失敗原因:
當 getstatic 指令把race的值取到操作棧頂(讀取,加載)時,volatile關鍵字保證了 rece 的值此時是正確的,
但是在執行 iconst_1、iadd這些指令(使用)時,其他線程可能已經把 race的值加大了,
而在操作棧頂的值就變成了過期的數據,所以 putstatic 指令(賦值,存儲,寫入)執行后就可能把較小的 race 值同步回內存之中。
客觀而言,在此使用字節碼分析并發問題并不嚴謹,因為即使編譯出來的只有一條字節碼指令,并不意味執行這條指令是一個原子操作。
由于 volatile 變量只能保證可見性,在不符合以下兩條規則的運算場景中,仍然要通過加鎖(使用?synchronize?或 java.util.concurrent中的原子類)來保證原子性。
1,運算結果并不依賴變量的當前值,或者能確保只有單一的線程修改變量的值。
2,變量不需要與其他的狀態變量共同參與不變約束。
像以下代碼所示的這類場景很適合使用?volatile?變量來控制并發,當showdown()?方法被調用時,能保證所有線程中執行的doWork()方法立即停下來:
//使用volatile變量來控制并發
public classVolatileVariableTest {volatile boolean shutdownRequested; //volatile變量
public voidshutdown() {
shutdownRequested= true;
}public voiddoWork() {while(!shutdownRequested) {//do sth.
}
}
1,2,volatile的特性 —– 禁止指令重排序優化
使用volatile變量的第二個語義是禁止指令重排序優化:
普通變量僅僅會保證在單線程的執行過程中所有依賴賦值結果的地方都能獲取到正確的結果,而不能保證變量賦值操作的順序與程序代碼中的執行順序一致。
我們在單線程的執行過程中是無法感知到這點,這也就是java 內存模型中描述的所謂的 “線程內表現為串行的語義”。
【指令重排序演示(偽代碼)】
Map configOptions;char[] configText;//此變量必須為 volatile
volatile boolean initialized = false;
1,//假設以下代碼在線程A 中執行//模擬讀取配置信息,當讀取完成后將 initialized 設置為true 已通知其他線程配置可用
configOptions = newHashMap();
configText=readConfigFile(filename);
processConfigOptions(configText, configOptions);
initialized= true;
2,//假設以下代碼在線程B 中執行//等待initialized 為true,代表線程A 已經把配置信息初始化完成
while(!initialized) {
sleep();
}//使用線程A 中初始化好的配置信息
doSomethingWithConfig();
}
如果定義initialized變量沒有使用volatile修飾:
就可能會由于指令重排序的優化,導致位于線程A 中最后一句碼initialized=true被提前執行(即這行代碼對應的匯編代碼被提前執行),
這樣在線程B中使用配置信息的代碼就可能出現錯誤(獲取時還沒有初始化配置),而volatile關鍵字則可以避免此類情況的發生。
PS:內存屏障的目的
因為緩存導致的可見性和,cpu/編譯期重排序執行優化可能導致錯誤。
不同的處理器重排序的規則也是不一樣的。
java內存模型為了避免這種差異造成的問題,通過內存屏障方式來實現可見見性和非重排序。
常見的有2種方式:1,通過 Synchronized關鍵字包住的代碼區域,插入了StoreStore屏障2,使用了volatile修飾變量,則對變量的寫操作,會插入StoreLoad屏障.
不常用的,通過Unsafe這個類來執行.
UNSAFE.putOrderedObject類似這樣的方法,會插入StoreStore內存屏障
Unsafe.putVolatiObject 則是插入了StoreLoad屏障
內存屏障 示例
volatile關鍵字是如何使用內存屏障禁止指令重排序優化的,
內存屏障(Memory Barrier),指重排序時不能把后面的指令重排序到內存屏障之前的位置。當兩個或以上的CPU訪問同一塊內存時,需要內存屏障來保證一致性。
為何說它禁止指令重排序呢?
從硬件上而言,指令重排序呢是指CPU采用允許將多條指令不按程序規定的順序分開發送給各相應電路單元處理。
并非說是任意重排,例如(a*b)+c?與?c+(a+b),代表c相加的指令是可以重排的。
比如屏障指令把修改同步到內存時,意味著所有之前的操作都已經執行完成,這便形成了“指令重排序無法越過內存屏障”的效果。
使用 volatile 還是 鎖?
眾多保障并發工具都使用了volatile?關鍵字,在某些情況下,它的同步機制性能要優于鎖(使用synchronized?關鍵字或java.util.concurrent包里的鎖),
但由于虛擬機對鎖實行的許多消除和優化,它并非快多少。
若讓volatile?關鍵字與鎖比較,可確定一個原則:
它讀操作的、性能消耗與普通變量幾乎無差別,寫操作可能較慢,因為需要在本地代碼中插入許多內存屏障指令來保證處理器不發生亂序執行。
但即便如此,大多數volatile?關鍵字的總開銷仍比鎖低,選擇的唯一依據是它的語義能否滿足使用常見的要求。
2. 對于 long 和 double 型變量的規則
(1)64位數據類型的非原子性協定
Java內存模型要求lock, unlock, read, load, assign, use,store,write這8個操作都具有原子性,
但對于64位的數據類型(long和double),在模型中特別定義了一條相對寬松的規定:
允許虛擬機將沒有被?volatile?修飾的64位數據的讀寫操作劃分為?兩次?32位的操作來進行,
即允許虛擬機實現選擇可以不保證64位數據類型的?load, store,read和write?這4個操作的原子性,
這點就是所謂的 long 和double 的非原子性協定(Nonatomic Treatment of double and long Variable)。
(2)非原子性協定導致的問題
如果有多個線程共享一個并未聲明為?volatile的 long 或 double類型的變量,并且同時對它們進行讀取和修改操作,
那么某些線程可能會讀取到一個既非原值,也不是其他線程修改值的代表了“半個變量”的數值。
(3)“半個變量”的情況
不過這種讀取到的“半個變量”的情況非常罕見(商業JVM中尚未出現):
因為Java內存模型雖然允許虛擬機不把long 和 double 變量的讀寫實現成原子操作,
但允許虛擬機選擇把 這些操作實現為具有原子性的操作,而且還強烈建議虛擬機這樣實現。
實際開發中,目前各平臺下的商用虛擬機幾乎選擇把64位數據讀寫操作作為原子操作來對待,
因此平時編寫代碼時不需要把long 和 double 變量專門聲明為?volatile。
并發過程三大特征 與 先行發生原則
1. 原子性、可見性與有序性
Java內存模型是圍繞著在并發過程中如何處理原子性,?可見性和有序性這3個特征來建立的。
(1)原子性(Atomicity)
由于Java內存模型來直接保證的原子性變量操作包括?read,load,assign,use,store和write,
我們大致認為基本數據類型的訪問讀寫數據是具備原子性的。
更大范圍的原子性保證
如果應用場景需要一個更大范圍的原子性保證,Java內存模型還提供了lock 和 unlock 操作來滿足這些需求,
盡管虛擬機沒有把lock 和 unlock 操作直接開放給用戶使用,
但是卻提供了更高層次的字節碼指令 monitorenter 和 monitorexit 來隱式地使用這兩個操作。
synchronized關鍵字
monitorenter?和?monitorexit?這兩個字節碼指令反映到java代碼中就是同步塊——synchronized關鍵字,因此在synchronized塊之間的操作也具備原子性。
(2)可見性(Visibility)
指當一個線程修改了共享變量的值,其他能夠立即得知這個修改。
Java內存模型是通過在變量修改后將新值同步回主內存,
在變量讀取前從主內存刷新變量值這種依賴主內存作為傳遞媒介的方式來實現可見性的,
無論是普通變量還是volatile變量都是如此
普通變量與 volatile變量的區別
volatile的特殊規則保證了新值能立即同步到主內存,以及每次使用前立即從主內存刷新,
所以volatile保證了多線程操作時變量的可見性,
普通變量則不能保證這一點。
synchronized 和 final關鍵字
除了volatile關鍵字外,Java還有兩個關鍵字實現可見性:?synchronized?和?final。
synchronized同步塊的可見性: 是由對一個變量執行unlock 操作前,必須先把此變量同步回主內存中;
final關鍵字的可見性:被final修飾的字段在構造器中一旦初始化完成,并且構造器沒有把this 的引用傳遞出去
(this引用傳遞很危險,其他線程很有可能通過此引用訪問到“初始化了一半”的對象),
那在其他線程中就能看見final 字段的值。
關于final關鍵字的可見性,以下代碼中的變量i 與 j 都具備可見性,它們無須同步就能被其他線程正確訪問:
(3)有序性(Ordering)
如果在本線程內觀察,所有的操作都是有序的;如果在一個線程中觀察另一個線程,所有的操作都是無序的
volatile和 synchronized關鍵字保證了線程間操作的有序性
volatile關鍵字本身就包含了禁止指令重排序的語義。
synchronized則是由 一個變量在同一時刻只允許一條線程對其進行lock 操作這條規則獲得的,這條規則決定了持有同一個鎖的兩個同步塊只能串行地進入。
2. 先行發生(happens-before)原則
若Java內存模型中所有的有序性都僅僅靠 volatile 和 synchronized 來完成,那么有些操作將很繁瑣,
而Java語言中的“先行發生”語言已考慮到,它是判斷斷數據是否存在競爭、線程是否安全的主要依據。
先行發生原則定義:
先行發生是Java內存模型中定義的兩項操作之間的偏序關系,
如果說操作A 先行發生于操作B,其實就是說在發生操作B之前,操作A產生的影響能被操作B觀察到,
影響包括 修改了內存中共享變量的值,發送了消息,調用了方法等。
PS:所謂產生影響能被后發生的觀察到,指的是前面發生的影響和后面發生的必須是有關聯,不然視作沒有影響。(即,有關聯的影響一定能觀察到)。
先行發生規則
下面是 Java內存模型下一些天然的先行發生關系,這些先行發生關系無須任何同步器協助就已經存在,可以在編碼中直接使用:
程序次序規則(Program Order Rule):在一個線程內,按照程序代碼順序,書寫在前面的操作先行發生于書寫在后面的操作,準確地說,應該是控制流順序。
管程鎖定規則(Monitor Lock Rule):一個unlock操作先行發生于后面對同一個鎖的lock操作;這里必須強調的是同一個鎖,而后面是指時間上的先后順序。
volatile變量規則(Volatile Variable Rule):對一個volatile變量的寫操作先行發生于后面對這個變量的讀操作,這里的后面是指時間上的先后順序。
線程啟動規則(Thread Start Rule): Thread對象的start() 方法先行發生于此線程的每一個動作。
線程終止規則(Thread Temination Rule):線程中的所有操作都先行發生于對此線程的終止檢測,可以通過Thread.join() 方法結束,
Thread.isAlive() 的返回值等手段檢測到線程已經終止運行。
線程中斷規則(Thread Interruption Rule):對線程interrupt() 方法的調用先行發生于被中斷線程的代碼檢測到中斷事件的發生,
可以通過 Thread.interrrupted() 方法檢測到是否有中斷發生。
對象終結規則(Finalizer Rule):一個對象的初始化完成先行發生于它的finalize() 方法的開始。
傳遞性(Transitivity):如果操作A 先行發生于操作B, 操作B 先行發生于操作C,那就可以得出操作A先行發生于操作C的結論。
時間先后順序 與 先行發生原則 關系探討
private int value = 0;public void setValue(intvalue) {this.value =value;
}public intgetValue() {returnvalue;
}
以上代碼是經常出現JavaBean中的?get/set方法,假設線程A 先調用了 setValue(1), 之后線程B 調用了同一個對象的getValue() ,那么線程B 收到的value是什么?
盡管線程A在操作時間上先于線程B, 但是無法確定線程B 中getValue()?方法的返回結果,換句話說,這里面的操作不是線程安全的。
解決辦法
要么把getter 和 setter方法都定義為 synchronized方法,這樣就可以套用管程鎖定規則;
要么把value定義為 volatile變量,由于setter方法對value的修改不依賴于value的原值,
滿足volatile關鍵字使用場景,這樣就可以套用volatile變量規則來實現先行發生關系;
一個操作 “時間上的先發生” 不代表這個操作會是“先行發生”。
那如果一個操作“先行發生”是否就能推導出這個操作必定是 “時間上的先行發生”呢?
此推理時不成立的。一個典型的例子就是多次提到的“指令重排序”,代碼如下:
//以下操作在同一個線程中執行
int i = 1;int j = 2;
根據程序次序規則,?int i = 1?的操作先行發生于?int j =2,但?int j = 2?完全可能先被處理器執行,這并不影響先行發生原則的正確性。
最終結論
時間先后順序與先行發生原則之間基本沒有太大的關系,所以我們衡量并發安全問題的時候不要受到時間順序的干擾,一切必須以先行發生原則為準。
總結
以上是生活随笔為你收集整理的java 类型不可视_jvm高级特性(5)(1)(原子性,可见性,有序性,volatile,概述)的全部內容,希望文章能夠幫你解決所遇到的問題。
                            
                        - 上一篇: java post webservice
 - 下一篇: linux发展前景方向(linux发展前