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