-
1 # 雲端的曼徹斯特
-
2 # 慎談奧秘
首先我們先了解一下volatile關鍵字的用法 ,volatile被喻為輕量級的"synchronized",它只是一個變數修飾符,只能用來修飾變數不能修飾方法和程式碼塊。
經典的用法:雙重校驗鎖實現單例
public class Singleton {
private volatile static Singleton singleton;
private Singleton (){}
public static Singleton getSingleton() {
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
上面這段程式使用了volatile關鍵字來修飾可能被多個執行緒同時訪問到的singleton
volatile與可見性:
先說一下可見性,所謂的可見性就是指可見性是指當多個執行緒訪問同一個變數時,一個執行緒修改了這個變數的值,其他執行緒能夠立即看得到修改的值。
快取一致性協議
現代處理器為了提高處理速度,在處理器和記憶體之間增加了多級快取,處理器不會直接去和記憶體通訊,將資料讀到內部快取中再進行操作。由於引入了多級快取,就存在快取資料不一致問題。
什麼是快取一致性協議呢?
每個處理器透過嗅探在總線上傳播的資料來檢查自己快取的值是不是過期了,當處理器發現自己快取行對應的記憶體地址被修改,就會將當前處理器的快取行設定成無效狀態,當處理器要對這個資料進行修改操作的時候,會強制重新從系統記憶體裡把資料讀到處理器快取裡。
volatile是兩條實現原則:
1.Lock字首指令會引起處理器快取會寫到記憶體
當對volatile變數進行寫操作的時候,JVM會向處理器傳送一條lock字首的指令,將這個快取中的變量回寫到系統主存中
2.一個處理器的快取回寫到記憶體會導致其他處理器的快取失效
處理器使用嗅探技術保證內部快取 系統記憶體和其他處理器的快取的資料在總線上保持一致。
綜合上面兩條實現原則,我們瞭解到:如果一個變數被volatile所修飾的話,在每次資料變化之後,其值都會被強制刷入主存。而其他處理器的快取由於遵守了快取一致性協議,也會把這個變數的值從主存載入到自己的快取中。這就保證了一個volatile在併發程式設計中,其值在多個快取中是可見的。
為了保證記憶體的可見性,除了快取一致性協議還有一個happends-before關係
注意:
陣列與物件例項中的 volatile,針對的是引用,物件獲陣列的地址具有可見性,但是陣列或物件內部的成員改變不具備可見性。
volatile寫—讀建立的happends-before關係
volatile的寫—讀與鎖的釋放—獲取有著相同的記憶體效果,所以說一個volatile變數的單個讀/寫操作,與一個普通變數的讀/寫操作都是使用同一個鎖來同步,執行效果是一樣的。先簡單介紹一下happends-before
happends-before法則
1.程式次序法則:按照程式碼順序執行
2.監視器鎖法則:一個unlock操作要先於同一個鎖的lock操作
3.volatile變數法則:對volatile域的寫入操作happends-before於每一個後續對同一域的讀操作
4.執行緒啟動法則:在一個執行緒裡,對Thread.start()的呼叫會先於Thread.run();
5.執行緒終結法則:執行緒中的任何動作都happends-before於其他執行緒檢測到這個執行緒已經終結,或者從Thread.join 呼叫中成功返回,或者Thread.isAlive返回false
中斷法則:一個執行緒呼叫另一個執行緒的interrupt.happens-before於被中斷的執行緒發現中斷。(透過跑出interruptedException,或者呼叫isInterrupted和interrupted)
6.終結法則:一個物件的建構函式的結束happends-before於這個物件finalizer的開始。
7.傳遞性:如果A happens-before於B, 且B happends-before 於C, 則A happens-before 於C
volatile變數法則:對volatile域的寫入操作happends-before於每一個後續對同一域的讀操作
1
當我們去寫一個volatile變數的時候,JMM會把該執行緒對應的本地記憶體中的共享變數值重新整理到主記憶體中,讀一個volatile變數的時候,JMM會把該執行緒對應的本地記憶體置為無效,接下來執行緒從主記憶體中讀取共享變數。兩個執行緒,執行緒A寫一個volatile變數,執行緒B隨後讀這個volatile變數。這個過程實際上就是執行緒A和執行緒B透過主記憶體進行通訊(執行緒間通訊)。
volatile與有序性
我們都知道多執行緒透過搶佔時間片來執行自己的程式碼體,所以我們會感覺到執行緒是同時執行完的,除了引入了時間片以外,由於處理器最佳化和指令重排等,CPU還可能對輸入程式碼進行亂序執行,比如我們拿到資料要執行寫庫,查詢,刪除這三個操作,這就會可能要涉及到有序性的問題了。
volatile可以禁止指令重排,這就保證了程式碼的程式會嚴格按照程式碼的先後順序執行。這就保證了有序性。被volatile修飾的變數的操作,會嚴格按照程式碼順序執行接下來我們就說一下為了實現volatile記憶體語義JMM是怎樣限制重排序(包括編譯器重排序和處理器重排序)的。
volatile重排序規則表(針對編譯器重排序):
從這張表我們可以看出:
當第一個操作是Volatile讀時,不管第二個操作是什麼,都不能重排序;
當第一個操作是Volatile寫時,第二個操作是Volatile讀或寫,不能重排序;
當第一個操作是普通讀寫,第二個操作是Volatile寫時,不能重排序。
記憶體屏障(針對處理器重排序):
編譯器在生成位元組碼時,會在指令序列中插入記憶體屏障來禁止特定型別的處理器重排序。(首先保證了正確性,再去追求執行效率)
1.在每個volatile寫操作前插入StoreStore屏障;對於這樣的語句Store1; StoreLoad; Store2,在Store2及後續寫入操作執行前,保證Store1的寫入操作對其它處理器可見。
2.在每個volatile寫操作後插入StoreLoad屏障;對於這樣的語句Store1; StoreLoad; Load2,在Load2及後續所有讀取操作執行前,保證Store1的寫入對所有處理器可見。
3.在每個volatile讀操作前插入LoadLoad屏障;對於這樣的語句Load1;LoadLoad; Load2,在Load2及後續讀取操作要讀取的資料被訪問前,保證Load1要讀取的資料被讀取完畢。
4.在每個volatile讀操作後插入LoadStore屏障;對於這樣的語句Load1; LoadStore; Store2,在Store2及後續寫入操作被刷出前,保證Load1要讀取的資料被讀取完畢。
如果編譯器無法確定後面是否還會有volatile讀或者寫的時候,為了安全,編譯器通常會在這裡插入一個StoreLoad屏障
volatile與原子性
因為volatile它不是鎖只是一個變數修飾符,所以無法保證原子性。
舉個栗子:
public class Test {
public volatile int i = 0;
public void increase() {
i++;
}
public static void main(String[] args) {
final Test test = new Test();
for(int i=0;i<10;i++){
new Thread(){
public void run() {
for(int j=0;j<1000;j++)
test.increase();
};
}.start();
}
while(Thread.activeCount()>1) //保證前面的執行緒都執行完
Thread.yield();
System.out.println(test.i);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
上面這段程式碼就是建立10個執行緒,然後分別執行1000次 i++操作。正常情況下,程式的輸出結果應該是10000,但是,多次執行的結果都小於10000。所以說volatile無法滿足原子性。
i++操作在編譯後位元組碼如下:
getfield #2 // Field i:I
iconst_1
iadd
putfield #2 // Field i:I
1
2
3
4
i++指令也包含了四個步驟,由於CPU按照時間片來進行執行緒排程的,只要是包含多個步驟的操作的執行,天然就是無法保證原子性的。因為這種執行緒執行,不像資料庫一樣可以回滾。如果一個執行緒要執行的步驟有5步,執行完3步就失去了CPU了,失去後就可能再也不會被排程,這怎麼可能保證原子性呢。
所以在以下兩個場景中可以使用volatile來代替synchronized:
1、運算結果並不依賴變數的當前值,或者能夠確保只有單一的執行緒會修改變數的值。
2、變數不需要與其他狀態變數共同參與不變約束。
———————————————— 河南新華
-
3 # IT資訊i
文章目錄一. volatile關鍵字是什麼?二. volatile兩種特性的體現三. 什麼樣的情況使用Volatile關鍵字?一. volatile關鍵字是什麼?
當一個變數定義為volatile之後,它將具備兩種特性:
①保證此變數對所有執行緒的可見性
當一條執行緒修改了這個變數的值,新值對於其他執行緒可以說是可以立即得知的。Java記憶體模型規定了有的變數都儲存在主記憶體,每條執行緒還有自己的工作記憶體,執行緒的工作記憶體儲存了該執行緒使用到的變數在主記憶體的副本複製,執行緒對變數的所有操作都必須在工作記憶體中進行,而不能直接讀取主記憶體中的變數。《深入理解Java虛擬機器第二版》P363
②禁止指令重排序最佳化
普通的變數僅僅會保證在該方法的執行過程中所有依賴該賦值結果的地方都能獲得正確的結果,而不能保證變數賦值操作的順序與程式程式碼中的執行順序一直。在一個執行緒的方法執行過程中無法感知到這點,故Java記憶體模型描述所謂的“執行緒內表示為序列的語義”《深入理解Java虛擬機器第二版》P369
二. volatile兩種特性的體現①保證此變數對所有執行緒的可見性
/** * 〈volatile關鍵字特性測試〉 * * @author 龍 * @create 2018/9/7 15:21 * @since 1.0.0 */public class Volatile { public static volatile int count=0; public static void main(String[] args) { for(int i=0;i<10;i++){ new Thread(//開啟10個執行緒 () -> { for(int j=0;j<100;j++){ count++;//每個執行緒執行加100 } System.out.print(count+" "); } ).start(); } }}//執行結果://130 130 230 330 430 588 588 688 788 888//100 200 300 400 500 600 700 800 900 1000//103 103 203 303 403 503 603 703 803 903上面的結果理想的結果是1000,然而卻出現了其它的結果,在併發執行的過程中,哪怕一次結果不對就認為這是不安全的。count++操作看似只有一條指令,但是在Java虛擬機器層面卻已經是幾條指令的組合,如讀取count,載入count,加一,儲存count,寫入count到主記憶體中。假設執行緒A在載入count之後,執行緒B也載入了count,兩個執行緒分別加一再寫回主記憶體,count就寫入了兩個相同的值,本應該是加二卻只是加一。注意這並不違背可見性,畢竟在B執行緒讀取count的時候,A執行緒並沒有改變count的值,則B執行緒可以說依然讀取的是count的正確的結果。
那麼我們是否可以對count的加一操作進行同步已達到正確的結果那?
/** * 〈volatile關鍵字特性測試〉 * * @author 龍 * @create 2018/9/7 15:21 * @since 1.0.0 */public class Volatile { //此處Volatile關鍵字可有可無,synchronized關鍵字保住了可見性 public static Integer count=0; public static void main(String[] args) { for(int i=0;i<10;i++){ new Thread( () -> { for(int j=0;j<100;j++){ add();//原子操作 } System.out.print(count+" "); } ).start(); } } public static synchronized void add(){ count++;//保證count加一的操作是原子的 }}//執行結果://200 300 400 200 500 600 700 800 900 1000 //100 200 300 437 500 600 700 800 900 1000 //100 200 300 465 500 600 700 800 900 1000可以說此時的結果已經是正確的了,原子性的加一操作就可以實現執行緒安全。
②禁止指令重排序最佳化
volatile boolean isOK = false;//假設以下程式碼線上程A執行 A.init();isOK=true;//假設以下程式碼線上程B執行while(!isOK){ sleep();}B.init();A執行緒在初始化的時候,B執行緒處於睡眠狀態,等待A執行緒完成初始化的時候才能夠進行自己的初始化。這裡的先後關係依賴於isOK這個變數。如果沒有volatile修飾isOK這個變數,那麼isOK的賦值就可能出現在A.init()之前(指令重排序,Java虛擬機器的一種最佳化措施),此時A沒有初始化,而B的初始化就破壞了它們之前形成的那種依賴關係,可能就會出錯。
三. 什麼樣的情況使用Volatile關鍵字?①確保它們自身狀態的可見性,如單例模式的雙重檢查加鎖實現
②標識一些重要的程式生命週期時間的發生(初始化或者關閉),如上述初始化程式碼
-
4 # 雲端的曼徹斯特
volatile解析成彙編指令是這樣的:
多cpu中各個核計算變數更新到二級或三級快取時如果控制代碼中有volatile標誌佔位符的話,
那麼會呼叫更新到記憶體的匯流排重新整理指令進行重新整理變數到記憶體,並且更新當前的快取版本號,當其他cpu核心訪問該資料時,發現版本號是舊的,根據mesh協議,會清空該快取卡槽,並且去記憶體取最新的變數並快取到本地快取中。
這樣就保證了可見性。
因為在java編譯生成位元組碼時會對程式碼執行順序進行篡改,以達到提升效能到最佳執行效率的效果。但是當javac程序看到volatile修飾的變數時,就不會啟動自動最佳化的機制。
這樣就保證了有序性。
-
5 # 慎談奧秘
首先我們先了解一下volatile關鍵字的用法 ,volatile被喻為輕量級的"synchronized",它只是一個變數修飾符,只能用來修飾變數不能修飾方法和程式碼塊。
經典的用法:雙重校驗鎖實現單例
public class Singleton {
private volatile static Singleton singleton;
private Singleton (){}
public static Singleton getSingleton() {
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
上面這段程式使用了volatile關鍵字來修飾可能被多個執行緒同時訪問到的singleton
volatile與可見性:
先說一下可見性,所謂的可見性就是指可見性是指當多個執行緒訪問同一個變數時,一個執行緒修改了這個變數的值,其他執行緒能夠立即看得到修改的值。
快取一致性協議
現代處理器為了提高處理速度,在處理器和記憶體之間增加了多級快取,處理器不會直接去和記憶體通訊,將資料讀到內部快取中再進行操作。由於引入了多級快取,就存在快取資料不一致問題。
什麼是快取一致性協議呢?
每個處理器透過嗅探在總線上傳播的資料來檢查自己快取的值是不是過期了,當處理器發現自己快取行對應的記憶體地址被修改,就會將當前處理器的快取行設定成無效狀態,當處理器要對這個資料進行修改操作的時候,會強制重新從系統記憶體裡把資料讀到處理器快取裡。
volatile是兩條實現原則:
1.Lock字首指令會引起處理器快取會寫到記憶體
當對volatile變數進行寫操作的時候,JVM會向處理器傳送一條lock字首的指令,將這個快取中的變量回寫到系統主存中
2.一個處理器的快取回寫到記憶體會導致其他處理器的快取失效
處理器使用嗅探技術保證內部快取 系統記憶體和其他處理器的快取的資料在總線上保持一致。
綜合上面兩條實現原則,我們瞭解到:如果一個變數被volatile所修飾的話,在每次資料變化之後,其值都會被強制刷入主存。而其他處理器的快取由於遵守了快取一致性協議,也會把這個變數的值從主存載入到自己的快取中。這就保證了一個volatile在併發程式設計中,其值在多個快取中是可見的。
為了保證記憶體的可見性,除了快取一致性協議還有一個happends-before關係
注意:
陣列與物件例項中的 volatile,針對的是引用,物件獲陣列的地址具有可見性,但是陣列或物件內部的成員改變不具備可見性。
volatile寫—讀建立的happends-before關係
volatile的寫—讀與鎖的釋放—獲取有著相同的記憶體效果,所以說一個volatile變數的單個讀/寫操作,與一個普通變數的讀/寫操作都是使用同一個鎖來同步,執行效果是一樣的。先簡單介紹一下happends-before
happends-before法則
1.程式次序法則:按照程式碼順序執行
2.監視器鎖法則:一個unlock操作要先於同一個鎖的lock操作
3.volatile變數法則:對volatile域的寫入操作happends-before於每一個後續對同一域的讀操作
4.執行緒啟動法則:在一個執行緒裡,對Thread.start()的呼叫會先於Thread.run();
5.執行緒終結法則:執行緒中的任何動作都happends-before於其他執行緒檢測到這個執行緒已經終結,或者從Thread.join 呼叫中成功返回,或者Thread.isAlive返回false
中斷法則:一個執行緒呼叫另一個執行緒的interrupt.happens-before於被中斷的執行緒發現中斷。(透過跑出interruptedException,或者呼叫isInterrupted和interrupted)
6.終結法則:一個物件的建構函式的結束happends-before於這個物件finalizer的開始。
7.傳遞性:如果A happens-before於B, 且B happends-before 於C, 則A happens-before 於C
volatile變數法則:對volatile域的寫入操作happends-before於每一個後續對同一域的讀操作
1
當我們去寫一個volatile變數的時候,JMM會把該執行緒對應的本地記憶體中的共享變數值重新整理到主記憶體中,讀一個volatile變數的時候,JMM會把該執行緒對應的本地記憶體置為無效,接下來執行緒從主記憶體中讀取共享變數。兩個執行緒,執行緒A寫一個volatile變數,執行緒B隨後讀這個volatile變數。這個過程實際上就是執行緒A和執行緒B透過主記憶體進行通訊(執行緒間通訊)。
volatile與有序性
我們都知道多執行緒透過搶佔時間片來執行自己的程式碼體,所以我們會感覺到執行緒是同時執行完的,除了引入了時間片以外,由於處理器最佳化和指令重排等,CPU還可能對輸入程式碼進行亂序執行,比如我們拿到資料要執行寫庫,查詢,刪除這三個操作,這就會可能要涉及到有序性的問題了。
volatile可以禁止指令重排,這就保證了程式碼的程式會嚴格按照程式碼的先後順序執行。這就保證了有序性。被volatile修飾的變數的操作,會嚴格按照程式碼順序執行接下來我們就說一下為了實現volatile記憶體語義JMM是怎樣限制重排序(包括編譯器重排序和處理器重排序)的。
volatile重排序規則表(針對編譯器重排序):
從這張表我們可以看出:
當第一個操作是Volatile讀時,不管第二個操作是什麼,都不能重排序;
當第一個操作是Volatile寫時,第二個操作是Volatile讀或寫,不能重排序;
當第一個操作是普通讀寫,第二個操作是Volatile寫時,不能重排序。
記憶體屏障(針對處理器重排序):
編譯器在生成位元組碼時,會在指令序列中插入記憶體屏障來禁止特定型別的處理器重排序。(首先保證了正確性,再去追求執行效率)
1.在每個volatile寫操作前插入StoreStore屏障;對於這樣的語句Store1; StoreLoad; Store2,在Store2及後續寫入操作執行前,保證Store1的寫入操作對其它處理器可見。
2.在每個volatile寫操作後插入StoreLoad屏障;對於這樣的語句Store1; StoreLoad; Load2,在Load2及後續所有讀取操作執行前,保證Store1的寫入對所有處理器可見。
3.在每個volatile讀操作前插入LoadLoad屏障;對於這樣的語句Load1;LoadLoad; Load2,在Load2及後續讀取操作要讀取的資料被訪問前,保證Load1要讀取的資料被讀取完畢。
4.在每個volatile讀操作後插入LoadStore屏障;對於這樣的語句Load1; LoadStore; Store2,在Store2及後續寫入操作被刷出前,保證Load1要讀取的資料被讀取完畢。
如果編譯器無法確定後面是否還會有volatile讀或者寫的時候,為了安全,編譯器通常會在這裡插入一個StoreLoad屏障
volatile與原子性
因為volatile它不是鎖只是一個變數修飾符,所以無法保證原子性。
舉個栗子:
public class Test {
public volatile int i = 0;
public void increase() {
i++;
}
public static void main(String[] args) {
final Test test = new Test();
for(int i=0;i<10;i++){
new Thread(){
public void run() {
for(int j=0;j<1000;j++)
test.increase();
};
}.start();
}
while(Thread.activeCount()>1) //保證前面的執行緒都執行完
Thread.yield();
System.out.println(test.i);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
上面這段程式碼就是建立10個執行緒,然後分別執行1000次 i++操作。正常情況下,程式的輸出結果應該是10000,但是,多次執行的結果都小於10000。所以說volatile無法滿足原子性。
i++操作在編譯後位元組碼如下:
getfield #2 // Field i:I
iconst_1
iadd
putfield #2 // Field i:I
1
2
3
4
i++指令也包含了四個步驟,由於CPU按照時間片來進行執行緒排程的,只要是包含多個步驟的操作的執行,天然就是無法保證原子性的。因為這種執行緒執行,不像資料庫一樣可以回滾。如果一個執行緒要執行的步驟有5步,執行完3步就失去了CPU了,失去後就可能再也不會被排程,這怎麼可能保證原子性呢。
所以在以下兩個場景中可以使用volatile來代替synchronized:
1、運算結果並不依賴變數的當前值,或者能夠確保只有單一的執行緒會修改變數的值。
2、變數不需要與其他狀態變數共同參與不變約束。
———————————————— 河南新華
-
6 # IT資訊i
文章目錄一. volatile關鍵字是什麼?二. volatile兩種特性的體現三. 什麼樣的情況使用Volatile關鍵字?一. volatile關鍵字是什麼?
當一個變數定義為volatile之後,它將具備兩種特性:
①保證此變數對所有執行緒的可見性
當一條執行緒修改了這個變數的值,新值對於其他執行緒可以說是可以立即得知的。Java記憶體模型規定了有的變數都儲存在主記憶體,每條執行緒還有自己的工作記憶體,執行緒的工作記憶體儲存了該執行緒使用到的變數在主記憶體的副本複製,執行緒對變數的所有操作都必須在工作記憶體中進行,而不能直接讀取主記憶體中的變數。《深入理解Java虛擬機器第二版》P363
②禁止指令重排序最佳化
普通的變數僅僅會保證在該方法的執行過程中所有依賴該賦值結果的地方都能獲得正確的結果,而不能保證變數賦值操作的順序與程式程式碼中的執行順序一直。在一個執行緒的方法執行過程中無法感知到這點,故Java記憶體模型描述所謂的“執行緒內表示為序列的語義”《深入理解Java虛擬機器第二版》P369
二. volatile兩種特性的體現①保證此變數對所有執行緒的可見性
/** * 〈volatile關鍵字特性測試〉 * * @author 龍 * @create 2018/9/7 15:21 * @since 1.0.0 */public class Volatile { public static volatile int count=0; public static void main(String[] args) { for(int i=0;i<10;i++){ new Thread(//開啟10個執行緒 () -> { for(int j=0;j<100;j++){ count++;//每個執行緒執行加100 } System.out.print(count+" "); } ).start(); } }}//執行結果://130 130 230 330 430 588 588 688 788 888//100 200 300 400 500 600 700 800 900 1000//103 103 203 303 403 503 603 703 803 903上面的結果理想的結果是1000,然而卻出現了其它的結果,在併發執行的過程中,哪怕一次結果不對就認為這是不安全的。count++操作看似只有一條指令,但是在Java虛擬機器層面卻已經是幾條指令的組合,如讀取count,載入count,加一,儲存count,寫入count到主記憶體中。假設執行緒A在載入count之後,執行緒B也載入了count,兩個執行緒分別加一再寫回主記憶體,count就寫入了兩個相同的值,本應該是加二卻只是加一。注意這並不違背可見性,畢竟在B執行緒讀取count的時候,A執行緒並沒有改變count的值,則B執行緒可以說依然讀取的是count的正確的結果。
那麼我們是否可以對count的加一操作進行同步已達到正確的結果那?
/** * 〈volatile關鍵字特性測試〉 * * @author 龍 * @create 2018/9/7 15:21 * @since 1.0.0 */public class Volatile { //此處Volatile關鍵字可有可無,synchronized關鍵字保住了可見性 public static Integer count=0; public static void main(String[] args) { for(int i=0;i<10;i++){ new Thread( () -> { for(int j=0;j<100;j++){ add();//原子操作 } System.out.print(count+" "); } ).start(); } } public static synchronized void add(){ count++;//保證count加一的操作是原子的 }}//執行結果://200 300 400 200 500 600 700 800 900 1000 //100 200 300 437 500 600 700 800 900 1000 //100 200 300 465 500 600 700 800 900 1000可以說此時的結果已經是正確的了,原子性的加一操作就可以實現執行緒安全。
②禁止指令重排序最佳化
volatile boolean isOK = false;//假設以下程式碼線上程A執行 A.init();isOK=true;//假設以下程式碼線上程B執行while(!isOK){ sleep();}B.init();A執行緒在初始化的時候,B執行緒處於睡眠狀態,等待A執行緒完成初始化的時候才能夠進行自己的初始化。這裡的先後關係依賴於isOK這個變數。如果沒有volatile修飾isOK這個變數,那麼isOK的賦值就可能出現在A.init()之前(指令重排序,Java虛擬機器的一種最佳化措施),此時A沒有初始化,而B的初始化就破壞了它們之前形成的那種依賴關係,可能就會出錯。
三. 什麼樣的情況使用Volatile關鍵字?①確保它們自身狀態的可見性,如單例模式的雙重檢查加鎖實現
②標識一些重要的程式生命週期時間的發生(初始化或者關閉),如上述初始化程式碼
回覆列表
volatile解析成彙編指令是這樣的:
多cpu中各個核計算變數更新到二級或三級快取時如果控制代碼中有volatile標誌佔位符的話,
那麼會呼叫更新到記憶體的匯流排重新整理指令進行重新整理變數到記憶體,並且更新當前的快取版本號,當其他cpu核心訪問該資料時,發現版本號是舊的,根據mesh協議,會清空該快取卡槽,並且去記憶體取最新的變數並快取到本地快取中。
這樣就保證了可見性。
因為在java編譯生成位元組碼時會對程式碼執行順序進行篡改,以達到提升效能到最佳執行效率的效果。但是當javac程序看到volatile修飾的變數時,就不會啟動自動最佳化的機制。
這樣就保證了有序性。