Volatile關鍵字&&DCL單例模式,volatile 和 synchronized 的區別

Life_Goes_On發表於2020-09-04

Volatile 英文翻譯:易變的、可變的、不穩定的。


一、volatile 定義及用法


多個執行緒的工作記憶體彼此獨立,互不可見,執行緒啟動的時候,虛擬機器為每個記憶體分配一塊工作記憶體,不僅包含了執行緒內部定義的區域性變數,也包含了執行緒所需要使用的共享變數的副本,是為了提高效率。

  • 在之前的示例中,執行緒不安全的問題,我們使用執行緒同步,也就是通過 synchronized 關鍵字,對操作的物件加鎖,遮蔽了其他執行緒對這塊程式碼的訪問,從而保證安全。

  • 這裡的 volatile 嘗試從另一個角度解決這個問題,那就是保證變數可見,有說法將其稱之為輕量級的synchronized。

volatile保證變數可見:簡單來說就是,當執行緒 A 對變數 X 進行了修改後,線上程 A 後面執行的其他執行緒能夠看到 X 的變動,就是保證了 X 永遠是最新的。更詳細的說,就是要符合以下兩個規則:

  1. 執行緒對變數進行修改後,要立刻寫回主記憶體;
  2. 執行緒對變數讀取的時候,要從主記憶體讀,而不是快取。

另一個角度,結合指令重排序的問題,volatile修飾的記憶體空間,在這上面執行的指令是禁止亂序的。因此,在單例模式的 DCL 寫法中,volatile是必須的元素。

1.1 示例1

private static int num = 0;
public static void main(String[] args) throws InterruptedException {
    new Thread(()->{
        while (num == 0){

        }
    }).start();

    Thread.sleep(1000);
    num = 1;
}

程式碼死迴圈,因為主執行緒裡的 num = 1,不能及時將資料變化更新到主存,因此上面的程式碼 while 條件持續為真。
因此可以給變數加上 volatile:

private volatile static int num = 0;

這樣就在執行幾秒後就會停止執行。

1.2 Double-Checked-Locking

在設計模式裡的單例模式,如果在多執行緒的情況下,仍然要保證始終只有一個物件,就要進行同步和鎖。

class DCL{
    private static volatile DCL instance;
    private DCL(){

    }

    public static DCL getInstance(){
        if (instance == null){//check1
            synchronized (DCL.class){
                if (instance == null){//check2
                    instance = new DCL();
                }
            }
        }
        return instance;
    }
}

雙重校驗鎖,實現執行緒安全的單例鎖。
volatile不能保證原子性。

1.3 原子性問題

原子操作就是這個操作要麼執行完,要麼不執行,不可能卡在中間。

比如 i = 2,這個指令就是具有原子性的,而 i++ 則不是,事實上 i++ 也是先拿 i,再修改,再重新賦值給 i 。

例如你讓一個volatile的integer自增(i++),其實要分成3步:

1)讀取volatile變數值到local;

2)增加變數的值;

3)把local的值寫回,讓其它的執行緒可見。

這3步的jvm指令為:

mov    0xc(%r10),%r8d ; Load
inc    %r8d           ; Increment
mov    %r8d,0xc(%r10) ; Store
lock addl $0x0,(%rsp) ; StoreLoad Barrier

最後一步是記憶體屏障。

什麼是記憶體屏障?

記憶體屏障告訴CPU和編譯器先於這個命令的必須先執行,後於這個命令的必須後執行,同時強制更新一次不同CPU的快取,也就是通過這個操作,使得 volatile 關鍵字達到了所謂的變數可見性。

這個時候我們就知道,一個操作並不是只有一步,而中間的幾步,如果其他的CPU修改了值,將會都產生覆蓋,還是會出現不安全的情況,這就導致了 volatile 無法保證原子性。

以前的 ++ 操作示例,加上 synchronized 後結果就正確了,如果用 volatile 不用 synchronized呢?示例程式碼:

public class NoAtomic {
    private static volatile int num = 0;
    public static void main(String[] args) throws InterruptedException {
        for (int i=0; i<100; i++){
            new Thread(()->{
                for (int j=0; j < 100; j++){
                    num++;
                }
            }).start();
        }
    Thread.sleep(3000);
    System.out.println(num);
    }
}

可以發現,輸出結果小於預期,雖然 volatile 保證了可見性,但是卻不能保證操作的原子性。
因此想要保證原子性,還是得回去找 synchronized 或者使用原子類。


二、volatile 和 synchronized 的區別


  • volatile 本質是在告訴 jvm 當前變數在暫存器(工作記憶體)中的值是不確定的,需要從主存中讀取;synchronized 則是鎖定當前變數,只有當前執行緒可以訪問該變數,其他執行緒被阻塞住。

  • volatile 僅能使用在變數級別;synchronized則可以使用在變數、方法、和類級別的。

  • volatile 僅能實現變數的修改可見性,不能保證原子性;而 synchronized 則可以保證變數的修改可見性和原子性。

  • volatile 不會造成執行緒的阻塞;synchronized 可能會造成執行緒的阻塞。

不過:現在基本不會用到 volatile,因為硬體層面,從工作記憶體到主存的更新速度已經提升的很快。

相關文章