天天看點

面試突擊44:volatile 有什麼用?

volatile 是 Java 并發程式設計的重要組成部分,也是常見的面試題之一,它的主要作用有兩個:保證記憶體的可見性和禁止指令重排序。下面我們具體來看這兩個功能。

記憶體可見性

說到記憶體可見性問題就不得不提 Java 記憶體模型,Java 記憶體模型(Java Memory Model)簡稱為 JMM,主要是用來屏蔽不同硬體和作業系統的記憶體通路差異的,因為在不同的硬體和不同的作業系統下,記憶體的通路是有一定的差異得,這種差異會導緻相同的代碼在不同的硬體和不同的作業系統下有着不一樣的行為,而 Java 記憶體模型就是解決這個差異,統一相同代碼在不同硬體和不同作業系統下的差異的。

Java 記憶體模型規定:所有的變量(執行個體變量和靜态變量)都必須存儲在主記憶體中,每個線程也會有自己的工作記憶體,線程的工作記憶體儲存了該線程用到的變量和主記憶體的副本拷貝,線程對變量的操作都在工作記憶體中進行。線程不能直接讀寫主記憶體中的變量,如下圖所示:

然而,Java 記憶體模型會帶來一個新的問題,那就是記憶體可見性問題,也就是當某個線程修改了主記憶體中共享變量的值之後,其他線程不能感覺到此值被修改了,它會一直使用自己工作記憶體中的“舊值”,這樣程式的執行結果就不符合我們的預期了,這就是記憶體可見性問題,我們用以下代碼來示範一下這個問題:

private static boolean flag = false;
public static void main(String[] args) {
    Thread t1 = new Thread(new Runnable() {
        @Override
        public void run() {
            while (!flag) {

            }
            System.out.println("終止執行");
        }
    });
    t1.start();
    Thread t2 = new Thread(new Runnable() {
        @Override
        public void run() {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("設定 flag=true");
            flag = true;
        }
    });
    t2.start();
}
           

以上代碼我們預期的結果是,線上程 1 執行了 1s 之後,線程 2 将 flag 變量修改為 true,之後線程 1 終止執行,然而,因為線程 1 感覺不到 flag 變量發生了修改,也就是記憶體可見性問題,是以會導緻線程 1 會永遠的執行下去,最終我們看到的結果是這樣的:

如何解決以上問題呢?隻需要給變量 flag 加上 volatile 修飾即可,具體的實作代碼如下:

private volatile static boolean flag = false;
public static void main(String[] args) {
    Thread t1 = new Thread(new Runnable() {
        @Override
        public void run() {
            while (!flag) {

            }
            System.out.println("終止執行");
        }
    });
    t1.start();
    Thread t2 = new Thread(new Runnable() {
        @Override
        public void run() {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("設定 flag=true");
            flag = true;
        }
    });
    t2.start();
}
           

以上程式的執行結果如下圖所示:

禁止指令重排序

指令重排序是指編譯器或 CPU 為了優化程式的執行性能,而對指令進行重新排序的一種手段。

指令重排序的實作初衷是好的,但是在多線程執行中,如果執行了指令重排序可能會導緻程式執行出錯。指令重排序最典型的一個問題就發生在單例模式中,比如以下問題代碼:

public class Singleton {
    private Singleton() {}
    private static Singleton instance = null;
    public static Singleton getInstance() {
        if (instance == null) { // ①
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new Singleton(); // ②
                }
            }
        }
        return instance;
    }
}
           

以上問題發生在代碼 ② 這一行“instance = new Singleton();”,這行代碼看似隻是一個建立對象的過程,然而它的實際執行卻分為以下 3 步:

  1. 建立記憶體空間。
  2. 在記憶體空間中初始化對象 Singleton。
  3. 将記憶體位址指派給 instance 對象(執行了此步驟,instance 就不等于 null 了)。

如果此變量不加 volatile,那麼線程 1 在執行到上述代碼的第 ② 處時就可能會執行指令重排序,将原本是 1、2、3 的執行順序,重排為 1、3、2。但是特殊情況下,線程 1 在執行完第 3 步之後,如果來了線程 2 執行到上述代碼的第 ① 處,判斷 instance 對象已經不為 null,但此時線程 1 還未将對象執行個體化完,那麼線程 2 将會得到一個被執行個體化“一半”的對象,進而導緻程式執行出錯,這就是為什麼要給私有變量添加 volatile 的原因了。

要使以上單例模式變為線程安全的程式,需要給 instance 變量添加 volatile 修飾,它的最終實作代碼如下:

public class Singleton {
    private Singleton() {}
    // 使用 volatile 禁止指令重排序
    private static volatile Singleton instance = null; // 【主要是此行代碼發生了變化】
    public static Singleton getInstance() {
        if (instance == null) { // ①
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new Singleton(); // ②
                }
            }
        }
        return instance;
    }
}
           

總結

volatile 是 Java 并發程式設計的重要組成部分,它的主要作用有兩個:保證記憶體的可見性和禁止指令重排序。volatile 常使用在一寫多讀的場景中,比如 CopyOnWriteArrayList 集合,它在操作的時候會把全部資料複制出來對寫操作加鎖,修改完之後再使用 setArray 方法把此數組指派為更新後的值,使用 volatile 可以使讀線程很快的告知到數組被修改,不會進行指令重排,操作完成後就可以對其他線程可見了。

是非審之于己,毀譽聽之于人,得失安之于數。

公衆号:Java面試真題解析

面試合集:https://gitee.com/mydb/interview

關注下面二維碼,訂閱更多精彩内容。

關注公衆号(加好友):

面試突擊44:volatile 有什麼用?

作者:

王磊的部落格

出處:

http://vipstone.cnblogs.com/