1. 程式人生 > 其它 >單例模式的雙重檢查鎖模式為什麼必須加 volatile?

單例模式的雙重檢查鎖模式為什麼必須加 volatile?

雙重檢查鎖模式的寫法


單例模式有多種寫法,我們重點介紹一下和 volatile 強相關的雙重檢查鎖模式的寫法,程式碼如下所示:

public class Singleton {

    private static volatile Singleton singleton;

    private Singleton() {
    }

    public static Singleton getInstance() {
        if (singleton == null) {
            synchronized (Singleton.class) {
                
if (singleton == null) { singleton = new Singleton(); } } } return singleton; } }

“為什麼要 double-check?去掉任何一次的 check 行不行?”

我們先來看第二次的 check,這時你需要考慮這樣一種情況:

有兩個執行緒同時呼叫getInstance方法,由於singleton是空的,因此兩個執行緒都可以通過第一重的 if 判斷;然後由於鎖機制的存在,會有一個執行緒先進入同步語句,並進入第二重 if 判斷 ,

而另外的一個執行緒就會在外面等待。

不過,當第一個執行緒執行完new Singleton()語句後,就會退出 synchronized 保護的區域,這時如果沒有第二重if (singleton == null) 判斷的話,那麼第二個執行緒也會建立一個例項,此時就破壞了單例,這肯定是不行的。

而對於第一個 check 而言,如果去掉它,那麼所有執行緒都會序列執行,效率低下,所以兩個 check 都是需要保留的。

在雙重檢查鎖模式中為什麼需要使用 volatile 關鍵字


相信細心的你可能看到了,我們在雙重檢查鎖模式中,給 singleton 這個物件加了 volatile 關鍵字,那為什麼要用 volatile 呢?

主要就在於 singleton = new Singleton() ,它並非是一個原子操作,事實上,在JVM中上述語句至少做了以下這3件事:

  • 第1步是給singleton分配記憶體空間;
  • 然後第2步開始呼叫Singleton的建構函式等,來初始化 singleton;
  • 最後第3步,將 singleton 物件指向分配的記憶體空間(執行完這步singleton就不是null了)。

這裡需要留意一下 1-2-3 的順序,因為存在指令重排序的優化,也就是說第2 步和第 3 步的順序是不能保證的,最終的執行順序,可能是 1-2-3,也有可能是 1-3-2

如果是 1-3-2,那麼在第 3 步執行完以後,singleton就不是null了,可是這時第 2 步並沒有執行,singleton 物件未完成初始化,它的屬性的值可能不是我們所預期的值。假設此時執行緒 2 進入 getInstance 方法,由於singleton已經不是null了,所以會通過第一重檢查並直接返回,但其實這時的 singleton 並沒有完成初始化,所以使用這個例項的時候會報錯,詳細流程如下圖所示:

  • 執行緒 1 首先執行新建例項的第一步,也就是分配單例物件的記憶體空間,由於執行緒 1 被重排序,所以執行了新建例項的第三步,也就是把 singleton 指向之前分配出來的記憶體地址,在這第三步執行之後,singleton 物件便不再是 null。
  • 這時執行緒 2 進入getInstance方法,判斷 singleton 物件不是 null,緊接著執行緒 2 就返回 singleton 物件並使用,由於沒有初始化,所以報錯了。最後,執行緒 1 “姍姍來遲”,才開始執行新建例項的第二步——初始化物件,可是這時的初始化已經晚了,因為前面已經報錯了。

使用了 volatile 之後,相當於是表明了該欄位的更新可能是在其他執行緒中發生的,因此應確保在讀取另一個執行緒寫入的值時,可以順利執行接下來所需的操作。在 JDK 5 以及後續版本所使用的 JMM 中,在使用了 volatile 後,會一定程度禁止相關語句的重排序,從而避免了上述由於重排序所導致的讀取到不完整物件的問題的發生。

到這裡關於“為什麼要用 volatile” 的問題就講完了,使用 volatile 的意義主要在於它可以防止避免拿到沒完成初始化的物件,從而保證了執行緒安全。

總結


在本課時中我們首先介紹了什麼是單例模式,以及為什麼需要使用單例模式,然後介紹了雙重檢查鎖模式這種寫法,以及面對這種寫法時為什麼需要 double-check,為什麼需要用 volatile?最主要的是為了保證執行緒安全。