前言
曾經(jīng)有遇到過這樣一個問題,有一個共享變量keepRunning=true,線程A中執(zhí)行while (keepRunning);,線程B中執(zhí)行keepRunning = false;,在main函數(shù)中同時開啟A,B線程,然后會發(fā)現(xiàn)程序會一直運(yùn)行且不會退出。說白了這其實就是一個典型的可見性問題,A線程并不知道keepRunning已經(jīng)被修改過了,故未將修改后的keepRunning變量的值從主內(nèi)存中讀取到線程緩存中來。
舉例
上面的問題等價于下面的代碼段:
/**
* @author mars_jun
*/
public class NoVisibility_Demonstration extends Thread {
boolean keepRunning = true;
public static void main(String[] args) throws InterruptedException {
NoVisibility_Demonstration t = new NoVisibility_Demonstration();
t.start();
System.out.println("start: " + t.keepRunning);
Thread.sleep(1000);
t.keepRunning = false;
System.out.println("end: " +t.keepRunning);
}
public void run() {
int x = 1;
while (keepRunning) {
//System.out.println("如果你不注釋這一行,程序會正常停止!");
x++;
}
System.out.println("x:" + x);
}
}
按上述代碼直接運(yùn)行,你會發(fā)現(xiàn)在打印完end: false之后,程序并沒有正常的退出,而是在一直跑著while (keepRunning)這個死循環(huán)。但是我們嘗試著將其中注釋的代碼System.out.println("如果你不注釋這一行,程序會正常停止!");給取消掉注釋,再運(yùn)行一次上面的代碼,就會發(fā)現(xiàn)程序會跑一段時間后正常退出??吹竭@里大家也許會感到奇怪,在進(jìn)行System.out.println這個IO操作后,線程t竟然讀到了主線程寫入的t.keepRunning = false這個值,然后導(dǎo)致while循環(huán)退出了。這里就不得不去看下println這個方法的源碼了。
public void println(String x) {
synchronized (this) {
print(x);
newLine();
}
}
這里我們會發(fā)現(xiàn)println方法是一個同步的方法。大家都知道用synchronized這個關(guān)鍵字修飾的方法或者代碼塊能保證代碼串行化的執(zhí)行(同一時間只能有一個線程獲取執(zhí)行權(quán)限),在Doug Lea大神的Concurrent Programming in Java一書中有這樣一個片段來描述synchronized這個關(guān)鍵字:
In essence, releasing a lock forces a flush of all writes from working memory employed by the thread, and acquiring a lock forces a (re)load of the values of accessible fields. While lock actions provide exclusion only for the operations performed within a synchronized method or block, these memory effects are defined to cover all fields used by the thread performing the action.
簡單翻譯一下:從本質(zhì)上來說,當(dāng)線程釋放一個鎖時會強(qiáng)制性的將工作內(nèi)存中之前所有的寫操作都刷新到主內(nèi)存中去,而獲取一個鎖則會強(qiáng)制性的加載可訪問到的值到線程工作內(nèi)存中來。雖然鎖操作只對同步方法和同步代碼塊這一塊起到作用,但是影響的卻是線程執(zhí)行操作所使用的所有字段。
這也就解釋了為什么加上System.out.println("如果你不注釋這一行,程序會正常停止!");這句代碼后,線程t能夠讀取到修改后的keepRunning的值了。對于這個問題上,有些人的說法是:打印是IO操作,而IO操作會引起線程的切換,線程切換會導(dǎo)致線程原本的緩存失效,從而也會讀取到修改后的值。這里我認(rèn)為這種說法也是有道理的,我嘗試著將打印換成File file = new File("G://1.txt");這句代碼,程序也能夠正常的結(jié)束。當(dāng)然,在這里大家也可以嘗試將將打印替換成synchronized(NoVisibility_Demonstration.class){ }這句空同步代碼塊,發(fā)現(xiàn)程序也能夠正常結(jié)束。
結(jié)論
針對上述問題,最起碼可以得出一個結(jié)論:當(dāng)進(jìn)行IO操作或者線程內(nèi)部調(diào)用synchronized修飾的方法或者同步代碼塊時,線程的緩存會進(jìn)行刷新,也就是會感知到共享變量的變化。當(dāng)然這也只是針對非volatile修飾的變量而言,當(dāng)變量被申明為volatile的時候,每次使用該變量都會從主內(nèi)存中進(jìn)行讀取。(這里對volatile不太熟悉的可以去看我的相關(guān)文章淺析volatile原理及其使用)
總結(jié)
只有在以下條件下,才能保證一個線程對字段的更改對其他線程可見:
- 寫入線程釋放同步鎖,讀取線程隨后獲取相同的同步鎖。釋放鎖的時候會強(qiáng)制從線程使用的工作內(nèi)存中刷新所有寫入,并且在獲取鎖的時候會強(qiáng)制重新加載可訪問字段的值。
- 如果一個字段被聲明為volatile,則寫入線程會立即將修改后的值同步到主內(nèi)存。讀取線程必須在每次訪問時重新加載volatile字段的值。
- 線程第一次訪問一個對象的某個字段時,它會看到字段的初始值或來自某個其他線程寫入的值。
- 當(dāng)一個線程終止時,所有寫入的變量都被刷新到主內(nèi)存。例如:現(xiàn)有線程A,B,在B線程中調(diào)用A.join(),那么在B中可以保證看到A線程產(chǎn)生的影響。