目標:
1.瞭解什麼是變量可見性問題
2.造成變量可見性問題原因
3.volatile關鍵字
前提說明:變量可見性問題發生在多線程,只有多線程纔有變量可見性問題
1.什麼是變量可見性問題
問題1:如何在多線程之間共享變量?使用全局變量:靜態變量或者共享對象
問題2:一個變量在線程1中被改變值了,在線程2中能看到此變量的最新值嗎?不一定
有如下代碼,執行結果是什麼呢?線程會停止循環打印出i的值嗎?
import java.util.concurrent.TimeUnit;
public class TestVolatile {
private static boolean isRunning = true;
public static void main(String[] args) {
new Thread(new Runnable() {
@Override
public void run() {
int i=0;
while (isRunning){
i++;
}
System.out.println(i);
}
}).start();
try {
TimeUnit.SECONDS.sleep(2); //兩秒之後
} catch (InterruptedException e) {
e.printStackTrace();
}
isRunning = false;
System.out.println("isRunning被設置爲false了");
}
}
結果線程並沒有停止,i的值並沒有打印出來。這就是併發中的變量可見性問題。即所見非所得。
怎麼才能可見呢?有兩種方式
1.加synchronized
2.加volatile關鍵字修飾
import java.util.concurrent.TimeUnit;
public class TestVolatile {
// private static boolean isRunning = true;
//第二種方式
private static volatile boolean isRunning = true;
public static void main(String[] args) {
new Thread(new Runnable() {
@Override
public void run() {
int i=0;
while (isRunning){
i++;
//第一種方式
// synchronized (this){
// i++;
// }
}
System.out.println(i);
}
}).start();
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
isRunning = false;
System.out.println("isRunning被設置爲false了");
}
}
2.造成變量可見性問題原因
先了解下jmm(java內存模型)規範
1.共享變量必須放在主內存中
2.線程有自己的工作內存,線程只可操作自己的工作內存
3.線程要操作共享變量,需要從主內存讀取到工作內存中,改變值後需要從工作內存同步到主內存中。
這就是線程安全問題根因,導致了變量可見性問題以及變量原子性問題,此處先討論可見性問題。
如圖顯示了線程的流程。
問題1:變量在內存之間同步的時候被緩存了嗎?
根據java內存模型我們可以知道變量修改完會同步到工作內存中。而且2秒肯定一定把變量同步到工作內存了。而內存之間的高速緩存並不是導致此問題的原因,這是更底層的開發者纔會涉及的問題。java開發者可以不考慮各內存之間高速緩存問題。
問題2:使用共享變量前不會重新從主內存讀取值嗎?問題我們先放下。再瞭解一下指令重排
java編程語言的語義允許java編譯器和微處理器進行執行優化,這些優化導致了與其交互的代碼不再同步,從而導致看似矛盾的行爲。
如圖所示,這是一個簡單的指令重排,導致結果跟代碼執行結果不一致。
那麼java中的指令重排什麼時候會發生?。
這就不得不說到java的jit編譯器(Just In Time Compiler)。java是解釋性語言及編譯性語言的混合體。
解釋執行:即腳本,在執行時候,由語言的解釋器將其一條條翻譯成機器可識別的指令。
編譯執行:將我們編寫的程序直接編譯成機器可以識別的指令碼。即把整個批量編譯。再分析一下代碼執行。
java把java文件編譯成類似字節碼文件,並加載到jvm進程中執行
jit此時就把循環這塊進行了isRunning緩存及指令重排。如上圖所示。這就導致他並不會重新從主內存讀取isRunning的值,並且主內存中此變量值改變了也不會導致線程中的while循環停止。所以此處就導致了可見性問題。
3.volatile關鍵字
解決此問題可以使用volatile關鍵字,即上文中的代碼中的第二種方式。第一種方式加synchronized解決原因先不討論。
根據jmm中規定的happen before和同步規則,對某個volatile字段的寫操作happends-before每個後續對該volatile字段的讀操作。對volatile變量v的寫入,與所有其它線程後續對v的讀同步。所以volatile關鍵字滿足一下功能:
1.禁止緩存;(volatile變量的訪問控制符會加個ACC_VOLATILE,此處可以參考oracle官網相應描述)
2.對volatile變量相關的指令不做重排序。
到此時我們就基本明白了變量可見性問題的原因及解決方案。
之後會繼續寫一下java中的原子操作以及java變量的原子性問題。