在測試volatile關鍵字如何保證數據在多個線程中的可見性問題的時候,引發的思考!
對於一個臨界資源,如果使用volatile關鍵字修飾,那麼就可以保證該變量在多個線程中可見。對於原理的理解不是很難,但是使用到代碼來模擬多線程問題的時候,對於何時從主存讀取共享變量何時將工作內存刷寫到主存的時機卻不是特別清楚。導致對於多線程理解不夠透徹!
1、線程的工作內存刷寫到主存以及從主存讀取到工作內存的時機
問題描述: 一個線程何時會從主存中去重新讀取共享變量的值,又是何時需要將工作內存的值重新刷寫到主存中。
前提: 不使用volatile關鍵字保證可見性的情況
主存和工作內存說明:
在多線程中,多個線程訪問主存中的臨界資源(共享變量)時,需要首先從主存中拷貝一份共享變量的值到自己的工作內存中,然後在線程中每次訪問該變量時都是訪問的線程工作內存(高速緩存)中的共享的變量副本,而不是每次都去主存中讀取共享變量的值(因爲CPU的讀寫速率和主存讀寫速率相差很大,如果CPU每次都訪問主存的話那麼效率會非常低)。當線程結束、IO操作導致線程切換、拋出異常等情況發生時會將自己工作內存中的值刷寫到主存中。
如果每個線程都使用自己工作內存中的共享變量值,而不去讀取主存中的值(主存中的共享變量值可能已經被修改),那麼就會造成多線程的數據同步問題。多線程中使用 volatile
關鍵字可以解決數據同步的問題。但 是現在要討論的問題是如果不使用volatile關鍵字,那麼什麼時候線程會重新去主存中讀取共享變量的值以及什麼時候會將工作內存刷寫會主存呢?
public class TestVolatitle1 {
public static void main(String[] args) throws InterruptedException {
ThreadDemo td = new ThreadDemo();
Thread threadA = new Thread(td);
threadA.start();
while (true) {
//System.out.println("打開這句以後,會進入if條件中,程序可正常結束!");
//或者是打開下面這句
//Thread.sleep(1);
if (td.isFlag()) {
System.out.println("------------------");
break;
}
}
}
}
class ThreadDemo implements Runnable {
private boolean flag = false;
@Override
public void run() {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
}
flag = true;
System.out.println("flag=" + isFlag());
}
public boolean isFlag() {
return flag;
}
public void setFlag(boolean flag) {
this.flag = flag;
}
}
代碼問題說明:
從上面這個測試程序中來看,簡單來講其實這就是一個可見性問題。共享變量 flag
沒有使用 volatile
關鍵字修飾,所以沒有線程間的內存可見性。控制檯輸出的結果爲: flag=true
然後卡住,程序結束不了。
但是當放開那句註釋掉的語句以後,程序可以正常結束。這說明了使用 System.out.println
語句會導致線程重新從主存中讀取共享變量的值到工作內存中。這裏並沒有使用volatile關鍵字去修飾共享變量,僅僅使用了一下System.out.println
語句。所以flag變量在各個線程中仍然不具有可見性,所以這裏主線程仍然不知道主線程的flag值已經改變,但是一定有某種原因使得主線程強制去主存中重新讀取了flag的值。
這是什麼原因導致的呢?
1、網上有一種說法是因爲 System.out.println
方法是一個用 synchronized
關鍵字修飾的同步方法。當執行完這個方法以後會釋放鎖,釋放鎖的操作會導致該線程重新從主存中讀取共享變量的值。(線程釋放鎖會強制刷寫到主存,線程獲取鎖會強制從主存重新刷新變量值)
從本質上來說,==當線程釋放一個鎖時會強制性的將工作內存中之前所有的寫操作都刷新到主內存中去,而獲取一個鎖則會強制性的加載可訪問到的值到線程工作內存中來。==雖然鎖操作只對同步方法和同步代碼塊這一塊起到作用,但是影響的卻是線程執行操作所使用的所有字段。
2、第二種說法是因爲System.out.println
方法是一個IO操作,IO操作會引起線程的切換,而線程的切換會導致線程原本的工作內存中緩存失效,然後去主存重新讀取共享變量的值(當線程切換到其他線程,後來又切換到該線程的時候去重新讀取)。爲了驗證是否IO操作會引起線程切換並且緩存失效,將打印語句換成: File file = new File("D://temp.txt");
測試結果依然會讓程序正常結束。
3、自己添加了一句 Thread.sleep(1);
代碼在主線程中的while循環中。結果程序仍然可以正常結束。針對於這種情況我在stack Overflow中看到了一個回答,說的是當執行該線程的cpu有空閒時,他會去主存讀取一下共享變量的值來更新線程工作內存中的值(意思就是說,在這裏我使用 sleep
使得執行該線程得CPU有了1毫秒的空閒時間,在這一毫秒的空閒時間中CPU重新去主存中讀取了共享變量的值)。如果按照這種說法,那麼就是說CPU何時去主存中重新讀取共享變量刷新緩存是一個不確定的因素(CPU有空閒時間就可以去重新讀取)。
問題: 在不使用volatile關鍵字的情況下,有哪些情況會導致線程的工作內存失效,然後必須重新去讀取主存的共享變量?
1、線程中釋放鎖時
2、線程切換時
3、CPU有空閒時間時(比如線程休眠時)
類似問題:
public class Test01 implements Runnable {
private int count = 10;
@Override
public /*synchronized*/ void run() {
count--;
//下面的語句涉及到IO操作,所以會導致線程切換,從而重新去主存讀取count值
System.out.println(Thread.currentThread().getName() + " count = " + count);
}
public static void main(String[] args) {
Test01 t = new Test01();
for (int i = 0; i < 5; i++) {
new Thread(t).start();
}
}
}
輸出結果:
Thread-0 ount = 7
Thread-3 ount = 5
Thread-4 ount = 6
Thread-1 ount = 7
Thread-2 ount = 7
這個結果不是固定的。造成這個問題的原因是,同時開啓了五個線程,五個線程把主存中的值拷貝到線程的工作內存中,然後執行 run()
方法在該方法中對count進行減一。遇到 System.out.println
語句後引起線程切換。導致了將工作內存中的count刷寫回主存,然後執行 System.out.println
時重新從主存中讀取值。如果對run方法加鎖即可解決該問題。