Double check 爲什麼須要 volatile?

對於單例的雙重檢鎖,爲什麼要對變量加上 volatile 修飾關鍵字?緩存

Prerequisite:對象建立的過程

要理解一個對象的建立過程,須要從運行時數據區進行分析,首先須要對JVM運行時數據區佈局有深刻的理解,同時掌握類加載過程當中各個階段的行爲。詳見後續的《從JVM角度分析 new 一個對象的詳細過程》。bash

本文無需深刻分析這兩個主題,只須要了解建立對象的整體流程便可。多線程

核心:非原子操做、指令重排序函數

private static volatile Singleton instance;

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

複製代碼

目的

避免重排序問題致使其餘線程看到一個已經分配內存和地址,可是沒有初始化的對象(對象還處於不可用狀態),就被其餘線程引用了(報異常)。佈局

建立對象的指令重排序分析

下面代碼在多線程環境下不是原子操做ui

instance = new Singleton();
複製代碼

這條指令坑被重排序,分以下兩種可能的指令排序場景。spa

場景1:不重排序(正常步驟)

正常的底層執行順序會分3步走:線程

一、給 instance 分配內存

二、調用實例 instance 的構造函數來初始化成員變量

三、將 instance 這個在棧中的引用,指向在步驟1和2中打包好的對象
複製代碼

不管線程A當前執行到一、二、3哪一步,對於線程B,可能看到的 instance 的狀態只有兩種:null 和 非 null。code

步驟 1 和 2 中的 instance 對象都是 null 的,第3步看到的是非 null,對於正常順序來講,這是沒問題的。對象

場景2:重排序

若是線程A 在重排序的狀況下,可能會變成 1,3,2,假如線程A執行到第二步「3」時,instance 雖然已經不是 null,但還沒初始化,不可用。

此時CPU時間片切換,從線程A 切換到線程B,線程B來調用 double check 這個 getInstance 單例方法,那麼第一個 null check 時,看到的 instance 引用因爲已經被線程 A 指向了內存塊,不爲 null,則直接返回這個instance。

可是,當使用這個對象的某個字段時,因爲還沒被初始化,處於不可用狀態,會致使異常發生。

解決方案:使用 volatile 修飾變量,禁止指令重排序

volatile 的原理

使用 volatile 修飾成員變量,那麼在變量賦值時,會有一個內存屏障,也就是說只有執行完123步操做以後,其餘線程讀取操做時才能看到 instance 這個變量的值,不會形成誤判,解決了對象狀態不完整的問題。

同時,volatile 會強制將緩存中修改的數據刷新到主內存中,確保對其餘線程的可見性。

此時,invalidate 其餘CPU的緩存行,當其餘CPU須要使用這個緩存行的變量時,就會去從新到主內存讀取,保證數據是最新的。

相關文章
相關標籤/搜索