volatile

volatile

  1. 線程間的可見性
  2. 防止指令重排

線程間的可見性

先了解下java的內存模型主內存和工作內存

java的內存模型主要目標是定義程序中各個變量的訪問規則,即虛擬機中將變量存儲到內存和從內存中取出變量這樣的底層細節。而這些變量包括實例字段、靜態字段和構成數組對象的元素。————摘自深入理解JVM第十二章

如上圖所示,每個線程都有自己獨立的工作內存而在這些工作內存中使用的變量需要從主內存中讀取或存儲到主內存中。

好,看下下面這段代碼(——來自Java多線程核心技術)

public class Run {
public static void main(String[] args) {
    try {
        RunThread runThread = new RunThread();
        runThread.start();
        Thread.sleep(1000);
        runThread.setRunning(false);
        System.out.println("已經賦值爲false");
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
}
}

class RunThread extends Thread {
private boolean isRunning = true;

public boolean isRunning() {
    return isRunning;
}

public void setRunning(boolean isRunning) {
    this.isRunning = isRunning;
}

@Override
public void run() {
    System.out.println("進入run了");
    while (isRunning == true) {

    }
    System.out.println("退出了!!!!");
}
}

陷入死循環了….. why?

有幾個線程?

2個 一個main,一個thread-0

答案:應該是3個還有個守護線程

 //在Thread.sleep(1000);下面加上這兩句
 System.out.println(Thread.activeCount());
 Thread.currentThread().getThreadGroup().list();


lock(鎖定):作用於主內存變量,把一個變量標示爲一條線程獨佔的狀態

unlock(解鎖):作用於主內存的變量,把一個處於鎖定狀態的變量釋放出來,釋放後的變量纔可以被其他線程鎖定

read(讀取):作用於主內存的變量,把一個變量的值從主內存傳輸到線程的工作內存中,以便隨後的load動作使用

load(載入):作用於工作內存的變量,把read操作從主存中得到的變量值放入工作內存的變量副本中

use(使用):作用於工作內存的變量,把工作內存中一個變量的值傳遞給執行引擎,每當虛擬機遇到一個需要使用到變量的值的字節碼指令時將會執行這個操作

assign(賦值):作用於工作內存的變量,把一個從執行引擎接收到的值賦給工作內存中的變量,每當虛擬機遇到一個給變量賦值的字節碼指令時執行這個操作

store(存儲):作用於工作內存的變量,把工作內存中一個變量的值傳送到主內存中,以便隨後的write操作使用

write(寫入):作用於主內存的變量,把store操作從工作內存中得到的變量的值放入主內存的變量中

分析


上圖表示是線程從主內存獲取變量到工作內存


上圖表示main將isRunning變量修改後更新到主內存

又上面兩張圖可知道雖然main線程更新了isRunning變量到主內存但是thread-0讀取的編程是自己工作內存中的信息isRunning=true,所以會一直陷入循環。


解決方案

使用volatile修飾isRunning變量

 private volatile boolean isRunning = true;


使用System.out.println()

while (isRunning == true) {
        System.out.println("在循環中!!");
    }

why?

下面是println方法的源碼,使用了synchronized塊
/**
 * Prints a String and then terminate the line.  This method behaves as
 * though it invokes <code>{@link #print(String)}</code> and then
 * <code>{@link #println()}</code>.
 *
 * @param x  The <code>String</code> to be printed.
 */
public void println(String x) {
    synchronized (this) {
        print(x);
        newLine();
    }
}

百度了下再看了下深入理解JVM發現做了鎖優化

鎖粗化

原則上我們在編寫代碼的時候,總是推薦同步塊的作用範圍限制得儘量小————只在共享數據的實際作域中才進行同步,這樣是爲了使用需要同步的操作數量儘可能變小,如果存在鎖競爭,那等待鎖的線程也能儘快拿到鎖。

大部分情況下,上面的原則都是正確的,但是如果一系列的連續操作都對同一個對象反覆加鎖和解鎖,甚至加鎖操作是出現在循環體中,那即使沒有線程競爭,頻繁的進行互斥同步操作也會導致不必要的性能損耗。如果虛擬機探測到有這樣一串零碎的操作對同一個對象鎖,將會把鎖同步範圍擴展(粗化)到整個操作序列的外部。

–深入理解JVM,13章線程安全與鎖優化

所以應該會變成這樣

synchronized(this){
    while(isRunning == true){
    }
}

synchronized規定,線程在加鎖時,先清空工作內存→在主內存中拷貝最新變量的副本到工作內存→執行完代碼→將更改後的共享變量的值刷新到主內存中→釋放互斥鎖。

爲什麼有這麼多在循環中!!! 應該是Thread.sleep(1000)的原因主線程先休眠1秒再配置isRunning


使用Thread.sleep()

while (isRunning == true) {
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

Thread.sleep()是阻塞線程並不釋放鎖,而是讓出cpu調度。 讓出cpu調度後下次執行會刷新工作內存。

所以什麼是可見性?

可見性指當一個線程修改了共享變量的值,其他線程能夠立即得知這個修改。


防止指令重排

指令重排

編譯器爲了提高程序的執行效率會按照一定的規則允許指令優化,不影響單線程程序執行結果,但是多線程就會影響程序結果。

參考深入理解JAVA虛擬機
Java多線程編程核心技術
https://www.cnblogs.com/LQBlog/p/8718735.html

如果有什麼錯誤歡迎指出來,感謝感謝!才學疏淺望諒解。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章