JAVA基礎 - volatile

目錄:

  1. 不可見性是什麼?
  2. volatile 可以保證原子性嗎?
  3. . 重排的示例和作用?
  4. Happends - before 是什麼?
  5. volatile與synchronized 區別?
  6. 參考

1.不可見性是什麼?

1.1不可見性案例

/**
 * 多線程修改變量 會出現 修改值之後不可見性
 */
public class VisibilityDemo1 {
    public static void main(String[] args) {
        MyThread thread = new MyThread();
        thread.start();
        while (true) {
            if (thread.isFlag()) {
                System.out.println(" 進入 -- ");
            }
        }
    }
}
class MyThread extends Thread {
    private boolean flag = false;
    @Override
    public void run() {
        try {
            Thread.sleep(1000);  // 標記1
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        flag = true;
        System.out.println(" flag = " + flag);
    }
    public boolean isFlag() {
        return flag;
    }
    public void setFlag(boolean flag) {
        this.flag = flag;
    }
}

控制檯會不會打印輸出:" 進入 –" ?,我估計應該有一部分讀者會認爲答應。
實際的輸出如下:
可見性案例

1.2 如何解決這個問題

   解決方案1:    去掉 標記1 代碼
   解決方案2private volatile boolean flag = false;
   解決方案3(下一篇 synchronized 進行分析 ):	加入 synchronized
    while (true) { 
            synchronized (VisibilityDemo1.class) {
                if (thread.isFlag()) {
                    System.out.println(" 進入 -- ");
                }
            }
        }	

1.3 原理分析

	分析之前需要知道JMM(JAVA 內存模型) :
		1.JAVA 內存模型 描述了java 程序中 各種變量(線程共享變量)的訪問規則,以及在JVM中將變量存儲到內存和從內存中讀取變量這樣的底層細節。
		2. JMM 有以下規定:
			  1. 所有的共享變量都存儲於主內存。這是所說的變量指的是實例變量和類變量。不包含局部變量,因爲局部變量時線程私有的。因此不存在競爭問題。
			  2.每一個線程還存在自己的工作內存,線程的工作內存,保留了被線程使用的變量的工作副本。
 			  3. 線程對變量的所有的操作(讀,取) 都必須在工作內存中完成,而不能直接讀寫主內存中的變量。
 			  4. 不同線程之間也不能直接訪問對方工作內存中的變量,線程間變量的值得傳遞需要通過主內存中轉完成

3. 主內存 和 工作內存示意圖

本地內存和主內存的關係

  1. 開始問題分析

    在這裏插入圖片描述

    問題發生的路徑:
    1. 子線程 thread 從主內存讀取到數據放入其對應的工作內存
    2.將 flag 的值 更改爲true , 但是這個時候 flag的值還沒有寫回主內存
    3.此時main 方法讀取到了flag的值爲false
    4. 當子線程t將 flag的值寫回去後,但是main 函數裏面的while(true) 調用的是系統比較底層
    的代碼,速度快,快到沒有時間再去讀取主內存中的值
    所以while(true) 讀取到點值一直是false

分析 解決方案2:(重要 重要 重要)
1.1 子線程t 從主內存讀取到數據放入其對應的工作
1.2 將flag的值更改爲true ,但是這個時候flag的值還沒有寫主內存
1.3 此時main方法 讀取到了flag的值爲false
1.4 當子線程t將flag的值寫回去後,失效( 嗅探技術 )其他線程對此變量副本
1.5 再次對flag進行 操作的時候線程會從主內存讀取最新的值,放入到工作線程中

           總結: volatile 保證不通線程對共享變量操作的可見性,也就是說一個線程修改了volatile的修飾的變量,當修改寫會內存時,另外一個線程立即看到最新值.

在這裏插入圖片描述

2. volatile 可以保證原子性嗎??

2.1  volatile 可以保證原子性嗎?
	答:不可以,  volatile是可見但是線程不安全。   // 解決辦法  synchronized 修飾
 public static void main(String[] args) {
        Runnable target = new MyTarget();
        for (int i = 1; i <= 100; i++) {
            new Thread(target, " 第" + i + "線程").start();
        }
    }
}
class MyTarget implements Runnable {
    private   volatile  int count = 0;
    @Override
    public void run() {
        for (int i = 1; i <= 10000; i++) {
            count++;
            System.out.println(Thread.currentThread().getName() + " count  =  " + count);
        }
    }
}

// 輸入結果  基本不可能等於 100*10000

分析一下:
1、從主內存中讀取數據到工作內存
2、對工作內存中的數據進行++ 操作
3、將工作內存中的數據寫回到主內存
也可以從編譯的字節碼看下
在這裏插入圖片描述
描述:
1. 假設此時X的值100,線程A需要對改變量進行自增1的操作,
首先它需要從主內存中讀取變量x的值。由於cpu的切換關係,此時cpu的執行權被切換了B線程。 A線程就需出於就緒狀態。B 線程出於運行狀態
2 線程b也需要從主內存中讀取X變量的值,由於線程A沒有對X值做任何
因此B讀取到點數據還是100
3 線程b工作內存中x 執行了+1操作,但是未刷新之主內存中
4 此時CPU的執行權切換到了A線程上,由於此時線程B沒有將工作內存中的數據刷新到主 內存,因 此A線程工作內存中的變量值還是1-- 沒有失效。
5 線程B將101 寫入到主內存
6 線程A將101 寫入到主內存
雖然計算量2次,但是隻對A進行了1次修改。

3.重排的實例和作用?

  1. 什麼是重排: 爲了提高性能,編譯器和處理器常常會對既定代碼執行順序進行指令排序.
    有三種情況:
    1.1 編譯器優化的重排序,編譯器在不改變單線程程序語義的前提下,可以重新安排語句的執行順序。
    1.2 指令級並行嗯重排序,現代處理器採用了指令級並行技術來將多條指令重疊執行,如果不存在數據依賴性,處理器可以改變 語句對應機器指令的執行順序。
    1.3 內存系統的重排序,由於處理器使用的緩存和讀/寫 緩衝區 ,這使得加載和存儲操作看上去可能是在亂序執行的。

    在這裏插入圖片描述

重點:重排序不是萬能的。 也會出問題。

在這裏插入圖片描述
其實這個地方還有 重要點 重要點 重要點.
重排序 返回內存地址引用 和 初始化構造方法詳細情況.

Happends - before 是什麼?

通過 happens-before的概念,描述操作之間內存的可見性。
如果一個操作執行的結果需要對另外一個操作可見,那麼這兩個操作之間必須存在happeds-before關係。
兩個操作可以是在一個線程內,也可以在不同線程之間。

一共有六項規則:
1.程序順序規則(單線程規則)
解釋:一個線程中的每個操作,happens-before於該線程中的任意後續操作。
- 同一個線程中前面的所有寫操作對後面的操作可見。

	2. 鎖規則(Synchronized, Lock 等)
    	解釋: 對一個鎖的解鎖,happens-before於隨後對這個鎖的加鎖。
    		如果線程1解鎖monitor,接着線程2鎖定了a,那麼線程1解鎖a之前的寫操作都對線程2可見(線程1和線程2可以是同一線程)
	
	3. volatile 變量規則
		 解釋 對一個鎖的解鎖,happends-before於隨後對這個鎖的加鎖。
		 如果線程1寫入了 volatile變量v(臨界資源) , 接着線程2鎖定了a,那麼,線程1解鎖a之前的寫操作都對線程2可見(線程1和線程2可以是同一線程)
	4: 傳遞性:
	     解釋: 如果A happens-before B  且 B happens-before C ,那麼 A happens-before C
	               A -> B   ,  B ->C 且  A-C
	
	5 start()規則
		解釋: 如果線程A執行操作ThreadB.start() 啓動線程B,那麼線程A的TheadB.start() 可以看到 A線程的變量。
		假定 線程線程A在執行過程中,通過執行ThreadB.start() 來啓動線程B,那麼線程A對共享變量的修改在接下來線程b開始執行前對線程B可見。 注意: 線程B 啓動之後,線程A在對變量修改線程B未必可見。
	
	6 join 規則
		解釋: 如果線程A 執行操作ThreadB.join 併成功返回。那麼線程B中的任意操作happens-before於線程A從ThreadB.join()操作成返回
				線程t1 寫入的所有變量,在任意其他線程t2調用t1.join,或者t1.isAlive成功返回後,都對t2可見。

demo: https://download.csdn.net/download/IT_peng/12203228

參考:

https://www.bilibili.com/video/av81907443?p=18 (這個視頻 質量挺高)
java併發編程的藝術
https://www.cnblogs.com/54chensongxia/p/11806836.html
https://blog.csdn.net/anjxue/article/details/51038466?depth_1-utm_source=distribute.pc_relevant.none-task&utm_source=distribute.pc_relevant.none-task
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章