首頁 >Java >java教程 >為什麼Java單例模式中需要加上volatile關鍵字?

為什麼Java單例模式中需要加上volatile關鍵字?

WBOY
WBOY轉載
2023-04-19 14:40:081252瀏覽

    前言:

    單例模式的實作方法有很多種,如餓漢模式、懶漢模式、靜態內部類和列舉等,當面試官問到「為什麼單例模式一定要加volatile?」時,那麼他指的是為什麼懶漢模式中的私有變數要加volatile?

    懶漢模式指的是對象的創建是懶加載的方式,並不是在程式啟動時就創建對象,而是第一次被真正使用時才創建對象。

    要解釋為什麼要加 volatile?我們先來看懶漢模式的具體實作程式碼:

    public class Singleton {
        // 1.防止外部直接 new 对象破坏单例模式
        private Singleton() {}
        // 2.通过私有变量保存单例对象【添加了 volatile 修饰】
        private static volatile Singleton instance = null;
        // 3.提供公共获取单例对象的方法
        public static Singleton getInstance() {
            if (instance == null) { // 第 1 次效验
                synchronized (Singleton.class) {
                    if (instance == null) { // 第 2 次效验
                        instance = new Singleton(); 
                    }
                }
            }
            return instance;
        }
    }

    從上述程式碼可以看出,為了確保執行緒安全性和高效能,程式碼中使用了兩次if 和synchronized 來保證程式的執行。那既然已經有 synchronized 來確保線程安全了,為什麼還要給變數加上 volatile 呢?在解釋這個問題之前,我們要先搞懂一個前置知識:volatile 有什麼用呢?

    1.volatile 作用

    volatile 有兩個主要的作用,第一,解決記憶體可見性問題,第二,防止指令重排序。

    1.1 記憶體可見性問題

    所謂記憶體可見性問題,指的是多個執行緒同時操作一個變量,其中某個執行緒修改了變數的值之後,其他執行緒感知不到變數的修改,這就是記憶體可見性問題。  而使用volatile 就可以解決記憶體可見性問題,例如以下程式碼,當沒有加入volatile 時,它的實作如下:

    private static boolean flag = false;
    public static void main(String[] args) {
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                // 如果 flag 变量为 true 就终止执行
                while (!flag) {
    
                }
                System.out.println("终止执行");
            }
        });
        t1.start();
        // 1s 之后将 flag 变量的值修改为 true
        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();
    }

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

    為什麼Java單例模式中需要加上volatile關鍵字?

     然而,上述程式執行了N 久之後,依然沒有結束執行,這表示執行緒2 在修改了flag 變數之後,執行緒1 根本沒有感知到變數的修改。

    那麼接下來,我們試著為flag 加上volatile,實作程式碼如下:

    public class volatileTest {
        private static volatile boolean flag = false;
        public static void main(String[] args) {
            Thread t1 = new Thread(new Runnable() {
                @Override
                public void run() {
                    // 如果 flag 变量为 true 就终止执行
                    while (!flag) {
    
                    }
                    System.out.println("终止执行");
                }
            });
            t1.start();
            // 1s 之后将 flag 变量的值修改为 true
            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();
        }
    }

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

    為什麼Java單例模式中需要加上volatile關鍵字?

     從上述執行結果我們可以看出,使用volatile 之後就可以解決程式中的記憶體可見性問題了。

    1.2 防止指令重排序

    指令重排序是指在程式執行過程中,編譯器或 JVM 常常會對指令重新排序,已提高程式的執行效能。指令重排序的設計初衷確實很好,在單線程中也能發揮很棒的作用,然而在多線程中,使用指令重排序就可能會導致線程安全問題了。

    所謂執行緒安全問題是指程式的執行結果,和我們的預期不符。例如我們預期的正確結果是 0,但程式的執行結果是 1,那麼這就是線程安全問題。

    而使用 volatile 可以禁止指令重新排序,從而保證程式在多執行緒執行時能夠正確執行。

    2.為什麼要用 volatile?

    回到主題,我們在單例模式中使用 volatile,主要是使用 volatile 可以禁止指令重新排序,從而保證程式的正常運作。這裡可能會有讀者提出疑問,不是已經使用了 synchronized 來確保線程安全嗎?那為什麼還要再加 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 主要是為了防止第② 處執行時,也就是「instance = new Singleton()」執行時的指令重排序的,這行程式碼看似只是一個創建物件的過程,然而它的實際執行卻分為以下3 步驟:

    • 建立記憶體空間。

    • 在記憶體空間中初始化物件 Singleton。

    • 將記憶體位址賦值給 instance 物件(執行了此步驟,instance 就不等於 null 了)。

    試想一下,如果不加volatile,那麼執行緒1 在執行到上述程式碼的第② 處時就可能會執行指令重排序,將原本是1、2、3 的執行順序,重排為1、3、2。但特殊情況下,執行緒1 在執行完第3 步驟之後,如果來了執行緒2 執行到上述程式碼的第① 處,判斷instance 物件已經不為null,但此時執行緒1 還未將物件實例化完,那麼線程2 將會得到一個被實例化「一半」的對象,從而導致程式執行出錯,這就是為什麼要給私有變數添加volatile 的原因了。

    以上是為什麼Java單例模式中需要加上volatile關鍵字?的詳細內容。更多資訊請關注PHP中文網其他相關文章!

    陳述:
    本文轉載於:yisu.com。如有侵權,請聯絡admin@php.cn刪除