Volatile的那些事

上一篇中,咱們瞭解了Synchronized關鍵字,知道了它的基本使用方法,它的同步特性,知道了它與Java內存模型的關係,也明白了Synchronized能夠保證「原子性」,「可見性」,「有序性」。今天咱們來看看另一個關鍵字Volatile,這也是極其重要的關鍵字之一。絕不誇張的說,面試的時候談到Synchronized,一定會談到Volatile。面試

一個小栗子

public class Main {
    private static boolean isStop = false;

    public static void main(String[] args) {
        new Thread(() -> {
            while (true) {
                if (isStop) {
                    System.out.println("結束");
                    return;
                }
            }
        }).start();

        try {
            TimeUnit.SECONDS.sleep(3);
            isStop = true;
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
複製代碼

首先定義了一個全局變量:isStop=false。而後在main方法裏面開了一個線程,裏面是一個死循環,當isStop=true,打印出一句話,結束循環。主線程睡了三秒鐘,把isStop改成true。安全

按道理來講,3秒鐘後,會打印出一句話,而且結束循環。可是,出人意料的事情發生了,等了好久,這句話遲遲沒有出現,也沒有結束循環。bash

這是爲何?這又和內存模型有關了,因而可知,內存模型是多麼重要,不光是Synchronized,仍是此次的Volatile都和內存模型有關。多線程

問題分析

咱們再來看看內存模型:app

image.png

線程的共享數據是存放在主內存的,每一個線程都有本身的本地內存,本地內存是線程獨享的。當一個線程須要共享數據,是先去本地內存中查找,若是找到的話,就不會再去主內存中找了,須要修改共享數據的話,是先把主內存的共享數據複製一份到本地內存,而後在本地內存中修改,再把數據複製到主內存。dom

若是把這個搞明白了,就很容易理解爲何會產生上面的狀況了:性能

isStop是共享數據,放在了主內存,子線程須要這個數據,就把數據複製到本身的本地內存,此時isStop=false,之後直接讀取本地內存就能夠。主線程修改了isStop,子線程是無感知的,仍是去本地內存中取數據,獲得的isStop仍是false,因此就形成了上面的狀況。ui

Volatile與可見性

如何解決這個問題呢,只須要給isStop加一個Volatile關鍵字:spa

public class Main {
    private static volatile boolean isStop = false;

    public static void main(String[] args) {
        new Thread(() -> {
            while (true) {
                if (isStop) {
                    System.out.println("結束");
                    return;
                }
            }
        }).start();

        try {
            TimeUnit.SECONDS.sleep(3);
            isStop = true;
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
複製代碼

運行,問題完美解決。線程

Volatile的做用:

  1. 當一個變量加了volatile關鍵字後,線程修改這個變量後,強制當即刷新回主內存。

  2. 若是其餘線程的本地內存中有這個變量的副本,會強制把這個變量過時,下次就不能讀取這個副本了,那麼就只能去主內存取,拿到的數據就是最新的。

正是因爲這兩個緣由,因此Volatile能夠保證「可見性」

Volatile與有序性

指令重排的基本概念就再也不闡述了,上兩節內容已經介紹了指令重排的基本概念。

指令重排遵照的happens-before規則,其中有一條規則,就是Volatile規則:

被Volatile標記的不容許指令重排。

因此,Volatile能夠保證「有序性」。

那內部是如何禁止指令重排的呢?在指令中插入內存屏障

內存屏障有四種類型,以下所示:

image.png

在生成指令序列的時候,會根據具體狀況插入不一樣的內存屏障。

總結下,Volatile能夠保證「可見性」,「有序性」

Volatile與單例模式

public class Main {
    private static Main main;

    private Main() {
    }

    public static Main getInstance() {
        if (main == null) {
            synchronized (Main.class) {
                if (main == null) {
                    main = new Main();
                }
            }
        }
        return main;
    }
}
複製代碼

這裏比較經典的單例模式,看上去沒什麼問題,線程安全,性能也不錯,又是懶加載,這個單例模式還有一個響噹噹的名字:DCL

可是實際上,仍是有點問題的,問題就出在

main = new Main();
複製代碼

這又和內存模型有關係了。執行這個建立對象會有3個步驟:

  1. 分配內存
  2. 執行構造方法
  3. 指向地址

說明建立對象不是原子性操做,可是真正引發問題的是指令重排。先執行2,仍是先執行3,在單線程中是無所謂的,可是在多線程中就不同了。若是線程A先執行3,還沒來得及執行2,此時,有一個線程B進來了,發現main不爲空了,直接返回main,而後使用返回出來的main,可是此時main還不是完整的,由於線程A尚未來得及執行構造方法。

因此單例模式得在定義變量的時候,加上Volatile,即:

public class Main {
    private volatile static Main main;

    private Main() {
    }

    public static Main getInstance() {
        if (main == null) {
            synchronized (Main.class) {
                if (main == null) {
                    main = new Main();
                }
            }
        }
        return main;
    }
}
複製代碼

這樣就能夠避免上面所述的問題了。

好了,這篇文章到這裏主要內容就結束了,總結全文:Volatile能夠保證「有序性」,「可見性」,可是沒法保證「原子性」

題外話

嘿嘿,既然上面說的是主要內容結束了,就表明還有其餘內容。

咱們把文章開頭的例子再次拿出來:

public class Main {
    private static boolean isStop = false;

    public static void main(String[] args) {
        new Thread(() -> {
            while (true) {
                if (isStop) {
                    System.out.println("結束");
                    return;
                }
            }
        }).start();

        try {
            TimeUnit.SECONDS.sleep(3);
            isStop = true;
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
複製代碼

若是既想讓子線程結束,又不想加Volatile關鍵字怎麼辦?這真的能夠作到嗎?固然能夠。

public class Main {
    private static boolean isStop = false;

    public static void main(String[] args) {
        new Thread(() -> {
            while (true) {
                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                if (isStop) {
                    System.out.println("結束");
                    return;
                }
            }
        }).start();

        try {
            TimeUnit.SECONDS.sleep(3);
            isStop = true;
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
複製代碼

在這裏,我讓子線程也睡了一秒,運行程序,發現子線程中止了。

public class Main {
    private static boolean isStop = false;

    public static void main(String[] args) {
        new Thread(() -> {
            while (true) {
                System.out.println("Hello");
                if (isStop) {
                    System.out.println("結束");
                    return;
                }
            }
        }).start();

        try {
            TimeUnit.SECONDS.sleep(3);
            isStop = true;
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
複製代碼

我把上面的讓子線程睡一秒鐘的代碼替換成 System.out.println,居然也成功讓子線程中止了。

public class Main {
    private static boolean isStop = false;

    public static void main(String[] args) {
        new Thread(() -> {
            while (true) {
                Random random=new Random();
                random.nextInt(150);
                if (isStop) {
                    System.out.println("結束");
                    return;
                }
            }
        }).start();

        try {
            TimeUnit.SECONDS.sleep(3);
            isStop = true;
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
複製代碼

這樣也能夠。

爲何呢?

由於JVM會盡力保證內存的可見性,即便這個變量沒有加入Volatile關鍵字,主要CPU有時間,都會盡力保證拿到最新的數據。可是第一個例子中,CPU不停的在作着死循環,死循環內部就是判斷isStop,沒有時間去作其餘的事情,可是隻要給它一點機會,就像上面的 睡一秒鐘,打印出一句話,生成一個隨機數,這些操做都是比較耗時的,CPU就可能能夠去拿到最新的數據了。不過和Volatile不一樣的是 Volatile是強制內存「可見性」,而這裏是可能能夠。

相關文章
相關標籤/搜索