線程的緩存什麼時候刷新?

前言

曾經有遇到過這樣一個問題,有一個共享變量keepRunning=true,線程A中執行while (keepRunning);,線程B中執行keepRunning = false;,在main函數中同時開啓A,B線程,而後會發現程序會一直運行且不會退出。說白了這其實就是一個典型的可見性問題,A線程並不知道keepRunning已經被修改過了,故未將修改後的keepRunning變量的值從主內存中讀取到線程緩存中來。html


舉例

上面的問題等價於下面的代碼段:緩存

 1/**
2 * @author mars_jun
3 */

4public class NoVisibility_Demonstration extends Thread {
5    boolean keepRunning = true;
6
7    public static void main(String[] args) throws InterruptedException {
8        NoVisibility_Demonstration t = new NoVisibility_Demonstration();
9        t.start();
10        System.out.println("start: " + t.keepRunning);
11        Thread.sleep(1000);
12        t.keepRunning = false;
13        System.out.println("end: " +t.keepRunning);
14    }
15
16    public void run() {
17        int x = 1;
18        while (keepRunning) {
19            //System.out.println("若是你不註釋這一行,程序會正常中止!");
20            x++;
21
22        }
23        System.out.println("x:" + x);
24    }
25}
複製代碼

按上述代碼直接運行,你會發如今打印完end: false以後,程序並無正常的退出,而是在一直跑着while (keepRunning)這個死循環。可是咱們嘗試着將其中註釋的代碼System.out.println("若是你不註釋這一行,程序會正常中止!");給取消掉註釋,再運行一次上面的代碼,就會發現程序會跑一段時間後正常退出。看到這裏你們也許會感到奇怪,在進行System.out.println這個IO操做後,線程t居然讀到了主線程寫入的t.keepRunning = false這個值,而後致使while循環退出了。這裏就不得不去看下println這個方法的源碼了。markdown

1    public void println(String x) {
2        synchronized (this) {
3            print(x);
4            newLine();
5        }
6    }
複製代碼

這裏咱們會發現println方法是一個同步的方法。你們都知道用synchronized這個關鍵字修飾的方法或者代碼塊能保證代碼串行化的執行(同一時間只能有一個線程獲取執行權限),在Doug Lea大神的Concurrent Programming in Java一書中有這樣一個片斷來描述synchronized這個關鍵字:app

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.ide

簡單翻譯一下:從本質上來講,當線程釋放一個鎖時會強制性的將工做內存中以前全部的寫操做都刷新到主內存中去,而獲取一個鎖則會強制性的加載可訪問到的值到線程工做內存中來。雖然鎖操做只對同步方法和同步代碼塊這一塊起到做用,可是影響的倒是線程執行操做所使用的全部字段。
這也就解釋了爲何加上System.out.println("若是你不註釋這一行,程序會正常中止!");這句代碼後,線程t可以讀取到修改後的keepRunning的值了。對於這個問題上,有些人的說法是:打印是IO操做,而IO操做會引發線程的切換,線程切換會致使線程本來的緩存失效,從而也會讀取到修改後的值。這裏我認爲這種說法也是有道理的,我嘗試着將打印換成File file = new File("G://1.txt");這句代碼,程序也可以正常的結束。固然,在這裏你們也能夠嘗試將將打印替換成synchronized(NoVisibility_Demonstration.class){ }這句空同步代碼塊,發現程序也可以正常結束。函數


結論

針對上述問題,最起碼能夠得出一個結論:當進行IO操做或者線程內部調用synchronized修飾的方法或者同步代碼塊時,線程的緩存會進行刷新,也就是會感知到共享變量的變化。固然這也只是針對非volatile修飾的變量而言,當變量被申明爲volatile的時候,每次使用該變量都會從主內存中進行讀取。(這裏對volatile不太熟悉的能夠去看個人相關文章淺析volatile原理及其使用oop


總結

只有在如下條件下,才能保證一個線程對字段的更改對其餘線程可見:ui

  1. 寫入線程釋放同步鎖,讀取線程隨後獲取相同的同步鎖。釋放鎖的時候會強制從線程使用的工做內存中刷新全部寫入,而且在獲取鎖的時候會強制從新加載可訪問字段的值。
  2. 若是一個字段被聲明爲volatile,則寫入線程會當即將修改後的值同步到主內存。讀取線程必須在每次訪問時從新加載volatile字段的值。
  3. 線程第一次訪問一個對象的某個字段時,它會看到字段的初始值或來自某個其餘線程寫入的值。
  4. 當一個線程終止時,全部寫入的變量都被刷新到主內存。例如:現有線程A,B,在B線程中調用A.join(),那麼在B中能夠保證看到A線程產生的影響。

END

相關文章
相關標籤/搜索