前言:
單例模式的實作方法有很多種,如餓漢模式、懶漢模式、靜態內部類和列舉等,當面試官問到「為什麼單例模式一定要加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 有什麼用呢?
volatile 有兩個主要的作用,第一,解決記憶體可見性問題,第二,防止指令重排序。
所謂記憶體可見性問題,指的是多個執行緒同時操作一個變量,其中某個執行緒修改了變數的值之後,其他執行緒感知不到變數的修改,這就是記憶體可見性問題。 而使用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(); }
以上程式的執行結果如下:
然而,上述程式執行了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(); } }
以上程式的執行結果如下:
從上述執行結果我們可以看出,使用volatile 之後就可以解決程式中的記憶體可見性問題了。
指令重排序是指在程式執行過程中,編譯器或 JVM 常常會對指令重新排序,已提高程式的執行效能。指令重排序的設計初衷確實很好,在單線程中也能發揮很棒的作用,然而在多線程中,使用指令重排序就可能會導致線程安全問題了。
所謂執行緒安全問題是指程式的執行結果,和我們的預期不符。例如我們預期的正確結果是 0,但程式的執行結果是 1,那麼這就是線程安全問題。
而使用 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中文網其他相關文章!