您好,登錄后才能下訂單哦!
要想更好的理解volatile關鍵字,我們先來聊聊基于高速緩存的存儲交互:
我們知道程序中進行計算的變量是存儲在內存中的,而處理器的計算速度和內存的讀取速度完全不在一個量級,區別猶如蘭博基尼和自行車。
要讓蘭博基尼開一小段就停下來等會自行車顯然不太合適,所以在處理器和內存之間加了一個高速緩存,高速緩存速度遠高于內存,猶如奔馳,雖然和蘭博基尼還有一定差距,每個處理器都對應一個高速緩存。
當要對一個變量進行計算的時候,先從內存中將該變量的值讀取到高速緩存中,再去計算,效率得到明顯提升,這是從硬件的的視角描述的內存。
Jvm虛擬機從另一個視角定義的內存模型規定所有變量都存儲在主內存中,每個線程有自己的工作內存,每個線程的工作內存只能被該線程獨占,其它線程不能訪問,所有的線程只能通過主內存來共享數據。
這里的主內存可以類比于硬件視角的內存,工作內存可以類比于硬件視角的高速緩存。
線程執行程序的時候先將主內存中的變量復制到工作內存中進行計算,計算完畢后再將變量同步到主內存中。
這么做雖然解決了執行效率的問題,但是同時也帶來了其它問題。
試想一下,線程A從主內存中復制了一個變量a=3到工作內存,并且對變量a進行了加一操作,a變成了4,此時線程B也從主內存中復制該變量到它自己的工作內存,它得到的a的值還是3,a的值不一致了。
用專業術語來說就是變量的可見性,此時變量a對于線程來說變得不可見了。
怎么解決這個問題?
volatile關鍵字閃亮登場:
當一個變量被定義為volatile之后,它對所有的線程就具有了可見性,也就是說當一個線程修改了該變量的值,所有的其它線程都可以立即知道,可以從兩個方面來理解這句話:
1.線程對變量進行修改之后,要立刻回寫到主內存。
2.線程對變量讀取的時候,要從主內存中讀,而不是工作內存。
但是這并不意味著使用了volatile關鍵字的變量具有了線程安全性,舉個栗子:
public class AddThread implements Runnable {
private volatile int num=0;
@Override
public void run() {
for (int i=1;i<=10000;i++){
num=num+1;
System.out.println(num);
}
}
}
public class VolatileThread {
public static void main(String[] args) {
Thread[] th = new Thread[20];
AddThread addTh = new AddThread();
for(int i=1;i<=20;i++){
th[i] = new Thread(addTh);
th[i].start();
}
}
}
這里我們創建了20個線程,每個線程對num進行10000次累加。
按理結果應該是打印1,2,3.。。。。。200000 。
但是結果卻是1,2,3…..x ,x小于200000.
為什么會是這樣的結果?
我們仔細分析一下這行代碼:num=num+1;
雖然只有一行代碼,但是被編譯為字節碼以后會對應四條指令:
1.Getstatic將num的值從主內存取出到線程的工作內存
2.Iconst_1 和 iadd 將num的值加一
3.Putstatic將結果同步回主內存
在第一步Getstatic將num的值從主內存取出到線程的工作內存因為num加了Volatile關鍵字,可以保證它的值是正確的,但是在執行第二步的時候其它的線程有可能已經將num的值加大了。在第三步就會將較小的值同步到內存,于是造成了我們看到的結果。
既然如此,Volatile在什么場合下可以用到呢?
一個變量,如果有多個線程只有一個線程會去修改這個變量,其它線程都只是讀取該變量的值就可以使用Volatile關鍵字,為什么呢?一個線程修改了該變量,其它線程會立刻獲取到修改后的值。
因為Volatile的特性可以保證這些線程獲取到的都是正確的值,而他們又不會去修改這個變量,不會造成該變量在各個線程中不一致的情況。當然這種場合也可以用synchronized關鍵字
當運算結果并不依賴變量的當前值的時候該變量也可以使用Volatile關鍵字,上栗子:
public class shutDownThread implements Runnable {
volatile boolean shutDownRequested;
public void shutDown(){
shutDownRequested = true;
}
@Override
public void run() {
while (!shutDownRequested) {
System.out.println("work!");
}
}
}
public class Demo01 {
public static void main(String[] args) throws InterruptedException {
Thread[] th = new Thread[10];
shutDownThread t = new shutDownThread();
for(int i=0;i<=9;i++){
th[i] = new Thread(t);
th[i].start();
}
Thread.sleep(2000);
t.shutDown();
}
}
當調用t.shutDown()方法將shutDownRequested的值設置為true以后,因為shutDownRequested 使用了volatile ,所有線程都獲取了它的最新值true,while循環的條件“!shutDownRequested”不再成立,“ System.out.println("work!");”打印work的代碼也就停止了執行。
Volatile還可以用來禁止指令重排序。
什么是指令重排序?
Int num1 = 3; 1
Int num2 = 4; 2
Int num3 = num1+num2; 3
在這段代碼中cpu在執行的時候會對代碼進行優化,以達到更快的執行速度,有可能會交換1處和2處的代碼執行的順序,這就是指令重排序。
指令重排序并不是為了執行速度不擇手段的任意重排代碼順序,這樣必然會亂套,重排序必須遵循一定的規則,1處和2處的代碼之間沒有任何關系,他們的執行順序對結果不會照成任何影響,也就是說1->2>3的執行和2->1->3的執行最后結果都為num3=7.我們說1處和2處的操作沒有數據依賴性,沒有數據依賴性的代碼可以重排序。
再看一下2處和3處的代碼,如果把他們交換順序,結果會不一樣,為什么會不一樣呢?因為這兩處操作都操作了num2這個變量,并且在第二處操作中修改了num2的值。
如果有兩個操作操作了同一個變量,并且其中一個為寫操作,那么這兩個操作就存在數據依賴性,對于有數據依賴性的操作,不能重排序,所以2處和3處的操作不能重排序。
還有一個規則是無論怎么重新排序,單線程的執行結果不能被改變,也就是說在單線程的情況下,我們是感受不到重排序帶來的影響的。
在多線程的情況下重排序會對程序造成什么影響呢?
舉個栗子:
//定義一個布爾型的變量表示是否讀取配置文件,初始為未讀取
Volatile boolean flag = false; 1
//線程A執行 讀取配置文件以后將flag改為true
readConfig(); 2
flag = true; 3
//線程B執行循環檢測flag,如果為false表示未讀取配置文件,則休眠。如果為true表示已讀取配置文件,則執行doSomething()
while(!flag){ 4
sleep(); 5
}
doSomething(); 6
在這段偽代碼中如果1處的代碼沒有用Volatile關鍵字,可能由于指令重排序的優化,在A線程中,3處的代碼 flag=true在2處代碼之前執行,導致B線程在配置文件還未讀取的情況下去執行相關操作,從而引起錯誤。
而Volatile關鍵字可以避免這種情況發生。
他是如何做到的呢?
通過匯編代碼可以看出,在3處當我們對Volatile修飾的變量做賦值操作的時候,多執行了一個指令 “lock add1 $0x0,(%esp)”.
這個指令的作用是使該指令之后的所有操作不能重排序到該指令的前面,專業術語叫做內存屏障。
正是因為內存屏障的存在能夠保證代碼的正確執行,所以讀取Volatile關鍵字修飾的變量和普通變量沒有什么差別,但是做寫入操作的時候由于要插入內存屏障,會影響到效率。
實際上在jdk對Synchronized進行優化以后,Synchronized的性能明顯提升和Volatile已經差別不大了,Volatile的用法比較復雜,容易出錯,Synchronized也可以解決變量可見性的問題,所以通常情況下我們優先選擇Synchronized,但是Synchronized不能禁止指令重排序,貌似這是Volatile的適用場合。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。