多線程線程安全之保證可見性

  • 在單線程的環境下,如果向一個變量先寫入一個值,然後在沒有寫干涉的情況下讀取這個變量的值,那這個時候讀取到的這個變量的值應該是之前寫入的那個值。這本來是一個很正常的事情。但是在多線程環境下,讀和寫發生在不同的線程中的時候,可能會出現:讀線程不能及時的讀取到其他線程寫入的最新的值。這就是所謂的可見性
  • 1.引出問題

一個測試代碼:

    public static boolean stop = false;

    public static void main(String[] args) throws InterruptedException {
//      創建線程
        Thread thread = new Thread(() -> {
            int i = 0;
//            判斷靜態變量是否爲true,如果不是則一直執行
            while (!stop) {
                i++;
                System.out.println("rs:" + i);
            }
        });
//        線程啓動
        thread.start();
//        睡眠一秒
        Thread.sleep(1000);
//        變更靜態變量
        stop = true;
    }

預計結果:

1.主線程執行,2.新線程啓動,3.主線程睡眠一秒,4.一秒之後子線程while判斷失效結束循環,線程結束。

測試結果:

while一直循環,程序無法停止。

  • 嘗試變更代碼得到預計結果

1.添加一條打印語句

    public static boolean stop = false;

    public static void main(String[] args) throws InterruptedException {
//      創建線程
        Thread thread = new Thread(() -> {
            int i = 0;
//            判斷靜態變量是否爲true,如果不是則一直執行
            while (!stop) {
                i++;
//                添加一條打印語句
                System.out.println("rs:" + i);
            }
        });
//        線程啓動
        thread.start();
//        睡眠一秒
        Thread.sleep(1000);
//        變更靜態變量
        stop = true;
    }

執行結果:

程序正常結束。

2.讓子線程睡眠

  public static boolean stop = false;

    public static void main(String[] args) throws InterruptedException {
//      創建線程
        Thread thread = new Thread(() -> {
            int i = 0;
//            判斷靜態變量是否爲true,如果不是則一直執行
            while (!stop) {
                i++;
//                讓子線程睡眠
                try {
                    Thread.sleep(0);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
//        線程啓動
        thread.start();
//        睡眠一秒
        Thread.sleep(1000);
//        變更靜態變量
        stop = true;
    }

執行結果:

程序正常結束

3.對靜態變量添加一個 volatile 屬性


    //添加volatile屬性
    public volatile static boolean stop = false;

    public static void main(String[] args) throws InterruptedException {
//      創建線程
        Thread thread = new Thread(() -> {
            int i = 0;
//            判斷靜態變量是否爲true,如果不是則一直執行
            while (!stop) {
                i++;
            }
        });
//        線程啓動
        thread.start();
//        睡眠一秒
        Thread.sleep(1000);
//        變更靜態變量
        stop = true;
    }

運行結果:

程序正常結束。

  • 嘗試

  1. print就可以導致循環結束

是因爲當前安裝的java虛擬機是server版本,server版本內部對代碼做了一個JIT深度優化(活性失敗)。

查詢當前jvm版本,如下圖:

while (!stop) {
    i++;
}
//這段代碼等價於

if (!stop) {
    while (true) {
        i++;
    }
}

如果想要jvm不對代碼進行優化可以在啓動參數中添加:-Djava.compiler=NONE

原始代碼測試運行:

程序正常結束。

查看 System.out.print(); 底層代碼,發現調用了

1.IO流

2.synchronized

那我們分別對代碼進行 IO操作,和 加鎖操作(記得刪掉啓動參數中的-Djava.compiler=NONE)


    public  static boolean stop = false;

    public static void main(String[] args) throws InterruptedException {
//      創建線程
        Thread thread = new Thread(() -> {
            int i = 0;
//            判斷靜態變量是否爲true,如果不是則一直執行
            while (!stop) {
                i++;
                synchronized (TestDemoDel.class){

                }
            }
        });
//        線程啓動
        thread.start();
//        睡眠一秒
        Thread.sleep(1000);
//        變更靜態變量
        stop = true;
    }


    public  static boolean stop = false;

    public static void main(String[] args) throws InterruptedException {
//      創建線程
        Thread thread = new Thread(() -> {
            int i = 0;
//            判斷靜態變量是否爲true,如果不是則一直執行
            while (!stop) {
                i++;
                new File("tea.txt") ;
            }
        });
//        線程啓動
        thread.start();
//        睡眠一秒
        Thread.sleep(1000);
//        變更靜態變量
        stop = true;
    }

 啓動,執行。發現也會結束循環,程序停止。

  • 概念

通過對上述代碼查看彙編指令,使用HSDIS工具,具體的使用方法自行百度。
可以看到,使用volatile關鍵字或各種方式使程序停止之後,多了一個Lock指令。

0x00000000037028f3: lock add dword ptr [rsp],0h ;*putstatic stop



0x0000000002b7ddab: push 0ffffffffc4834800h ;*putstatic stop; -

硬件層面
CPU/內存/IO設備

  • CPU層面增加了高速緩存
  • 操作系統,進程、線程、| CPU時間片來切換
  • 編譯器的優化 ,更合理的利用CPU的高速緩存.

看下圖:

高速緩存分爲 L1,L2,L3

  1. L1d 數據緩存
  2. L1i 指令緩存
  3. L2二級緩存
  4. L3三級緩存

離線程越近,速度越高。

當一個線程不存在某條內容數據時,會去內存中加載,加載之後,會讀取到CPU的高速緩存中,下一次會先從高速緩存中加載,如果緩存存在,那就可以直接使用。大大加快了線程IO性能的開銷,從而提升性能的以及CPU的利用率。

當使用 volatile 關鍵字時,會對緩存加鎖。使其從內存中讀取數據。所以會使其線程可見

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