1. 程式人生 > 其它 >對DCL(雙重檢查鎖 Double Check Lock)及Volatile原理的理解

對DCL(雙重檢查鎖 Double Check Lock)及Volatile原理的理解

技術標籤:JAVAjava併發程式設計jvm

Java建立物件過程

class T{
    int m=8;
}
T t=new T();

編譯成彙編指令:

new #2 <T>
dup
invokespecial #3 <T.<init>>
astore_1
return

當執行到new的時候,程式會申請一塊記憶體,由於成員變數中有一個int的m所以在該記憶體中有一個變數m而且初始化為0。

當執行到invokespecial的時候會呼叫建構函式把記憶體中的m的值該為8,執行到astore_1的時候會把變數t和new申請到的記憶體建立關聯

案例3

private int num=8;
    public T01(){
        new Thread(()->System.out.println(this.num)).start();
    }
    public static void main(String[] args) throws IOException {
        new T01();
        System.in.read();
    }

在main執行緒中new T01();的時候會有一個區域性變數this和new申請的記憶體空間建立關聯,當 new T01();編譯成彙編指令執行到invokespecial的時候就會開啟一個執行緒並執行。


這是this指向的是new剛申請的記憶體空間中的num,而剛申請的記憶體空間中的num的初始化值為0,那麼打印出來的值可能是0。

即當invokespecial指令和astore_1指令執行順序傳送亂序的是就會出現this溢位問題,所以建議在建構函式中建立執行緒但不能在建構函式中開啟該執行緒。

案例4

本案例中使用單例模式開始理解DCL(Double Check Lock),為了方便理解先從簡單案例開始不斷的優化

public class T01 {
    private static final T01 INSTANCE=new T01();

    private T01(){}
    public
static T01 getInstance(){ return INSTANCE; } public static void main(String[] args){ T01 t1=T01.getInstance(); T01 t2=T01.getInstance(); System.out.println(t1==t2); } }

存在的問題:不管該物件你有沒有使用,它都存在於記憶體空間中。即佔用記憶體空間

優化(判斷物件的引用是否為空):

public class T01 {
    private static T01 INSTANCE=new T01();

    private T01(){}
    public static T01 getInstance(){
        if(INSTANCE==null){
            try {
                Thread.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            INSTANCE=new T01();
        }
        return INSTANCE;
    }
    public static void main(String[] args){
        for(int i=0;i<100;i++){
            new Thread(new Runnable() {
                @Override
                public void run() {
                    System.out.println(T01.getInstance().hashCode());
                }
            }).start();
        }
    }
}

存在問題:當該程式處於多執行緒環境中的時候就會導致你獲取到的物件不是同一個

優化(給getInstance函式使用物件鎖):

public class T01 {
    private static T01 INSTANCE=new T01();

    private T01(){}
    public static synchronized T01 getInstance(){
        if(INSTANCE==null){
            try {
                Thread.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            INSTANCE=new T01();
        }
        return INSTANCE;
    }
    public static void main(String[] args){
        for(int i=0;i<100;i++){
            new Thread(new Runnable() {
                @Override
                public void run() {
                    System.out.println(T01.getInstance().hashCode());
                }
            }).start();
        }
    }
}

存在問題:由於在gatInstance函式中添加了物件鎖,那麼只能等到該執行緒執行完getInstance函式的時候,第二個執行緒才能呼叫該函式。

這樣子可以保證了併發程式設計的原子性,但是如果是getInstance函式體中的業務邏輯程式碼過長那麼就會導致第二個執行緒等待時間過長(即該物件鎖的粒度太大)

優化(判斷物件的引用是否為空後給本類新增物件鎖):

public class T01 {
    private static T01 INSTANCE=new T01();

    private T01(){}
    public static T01 getInstance(){
        //業務程式碼
        //.......
        
        if(INSTANCE==null){
            synchronized (T01.class){
                try {
                    Thread.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                INSTANCE=new T01();
            }
        }
        return INSTANCE;
    }
    public static void main(String[] args){
        for(int i=0;i<100;i++){
            new Thread(new Runnable() {
                @Override
                public void run() {
                    System.out.println(T01.getInstance().hashCode());
                }
            }).start();
        }
    }
}

存在問題:在多執行緒併發環境中不能保證getInstance函式獲取到的物件都是同一個

優化(雙重檢查鎖(Double Check Lock)):

public class T01 {
    private static T01 INSTANCE=new T01();

    private T01(){}
    public static T01 getInstance(){
        //業務程式碼
        //.......

        if(INSTANCE==null){
            synchronized (T01.class){
                if(INSTANCE==null){
                    try {
                        Thread.sleep(1);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    INSTANCE=new T01();
                }
            }
        }
        return INSTANCE;
    }
    public static void main(String[] args){
        for(int i=0;i<100;i++){
            new Thread(new Runnable() {
                @Override
                public void run() {
                    System.out.println(T01.getInstance().hashCode());
                }
            }).start();
        }
    }
}

1、給本類新增物件鎖前後兩次判斷INSTANCE是否為空,第一層判斷是為了提高效率,即當有一個執行緒new出來物件後第二個執行緒就不用競爭第一個執行緒的物件鎖。

2、結合前面幾個案例,由於Java建立物件過程中會成以下5條彙編指令

new #2 <T>
dup
invokespecial #3 <T.<init>>
astore_1
return

DCL存在的問題:當invokespecial和astore_1傳送指令重排序的時候就會導致在沒呼叫本類中的建構函式時就會將一個變數和new申請的記憶體空間給繫結。

由於new申請的記憶體空間預設給本類中的成員變數初始化為0或者null。則此時通過getInstance函式獲取的物件就會無法獲取真正的資料。

併發程式設計中阻止指令重排序

1、併發程式設計中阻止指令重排序分CPU層面和JVM層面來解決這個問題。

2、指令重排序的原理:以我們在公共廁所排隊等待為例,當你需要上廁所的時候剛好公共廁所中的所有廁所都有人了,你只能等到其中一個人上完廁所後你才可以使用該廁所。問題就在於只要有一個人上完廁所這個條件滿足就可以了,不需要關心到底是哪個廁所,指令重排序的原理與類似。

3、那麼為了讓指定的人去使用指定的廁所那麼可以在這些廁所之間加個屏障就可以了

CPU層面

記憶體屏障是一個特殊指令,當看到這種指令的時候,前面的必須執行完,後面的才能執行

Intel:ifence sfence mfence(CPU特有指令)

JVM層面

1、所有實現JVM規範的虛擬機器,必須實現四個屏障:LoadLoad LoadStore StoreLoad StoreStore(Load可以理解為讀,Store可以理解為寫)

LoadLoad屏障:對於這樣的語句load1;LoadLoad;Load2,在Load2及後續讀取操作的資料被訪問前,保證Load1要讀取的資料必須讀取完畢

StoreStore屏障:對於這樣的語句Store1,StoreStore,Store2,在Store2及後續寫入操作執行前;保證Store1的寫入操作對其他處理器可見

StoreLoad屏障:對於這樣的語句Load1;LoadStore;Store2;在Store2及後續寫入操作被刷出前,保證Load1要讀取的資料被讀取完畢

StoreLoad屏障:對於這樣的語句Store1;StoreLoad;Load2,在Load2及後續所有讀取操作執行前,保證Store的寫入對所有處理器可見

2、volatile修飾的內容,不可以重排序,對volatile修飾變數的讀寫訪問,都不可以換順序

volatile實現原理理解:

在這裡插入圖片描述

1、當對Volatie修飾的變數寫入的時候,在JVM中會在該變數前後新增StoreStore、StoreLoad。即當的對該變數進行寫入的時候前面所有的寫入操作必須完成,才能對該變數進行寫入。後面的所有讀操作要等對該變數寫入完成後才能開始讀

2、當對Volatile修飾的變數讀取的時候,在JVM中會在該變數前後新增LoadLoad、LoadStore。即當對該變數進行讀取的時候,我必須讀取完別人才能讀或者寫