死磕Java——volatile的理解

一、死磕Java——volatile的理解

1.1.JMM內(nèi)存模型

理解volatile的相關(guān)知識前,先簡單的認識一下JMM(Java Memory Model),JMM是jdk5引入的一種jvm的一種規(guī)范,本身是一種抽象的概念,并不真實存在,它屏蔽了各種硬件和操作系統(tǒng)的訪問差異,它的目的是為了解決由于多線程通過共享數(shù)據(jù)進行通信時,存在的本地內(nèi)存數(shù)據(jù)不一致、編譯器會對代碼進行指令重排等問題。

JMM有關(guān)同步的規(guī)定:

  • 線程解鎖前,必須把共享變量的值刷新回主內(nèi)存;
  • 線程加鎖前,必須讀取主內(nèi)存的最新值到自己的工作內(nèi)存中;
  • 加鎖和解鎖使用的是同一把鎖;

關(guān)于上述規(guī)定如下圖解:

說明:當我們在程序中new一個user對象的時候,這個對象就存在我們的主內(nèi)存中,當多個線程操作主內(nèi)存的name變量的時候,會先將user對象中的name屬性進行拷貝一份到自己線程的工作內(nèi)存中,自己修改自己工作內(nèi)存中的屬性后,再將修改后的屬性值刷新回主內(nèi)存,這就會存在一些問題,例如,一個線程寫完,還沒有寫回到主內(nèi)存,另一個線程先修改后寫入到主內(nèi)存,就會存在數(shù)據(jù)的丟失或者臟數(shù)據(jù)。所以,JMM就存在如下規(guī)定:

  • 可見性
  • 原子性
  • 有序性

1.2.Volatile關(guān)鍵字

volatile是java虛擬機提供的一種輕量級的同步機制,比較與synchronized。我們知道的事volatile的三大特性:

  • 可見性
  • 不保證原子性
  • 禁止指令重排

1.2.1.Volatile如何保證可見性

可見性就是當多個線程操作主內(nèi)存的共享數(shù)據(jù)的時候,當其中一個線程修改了數(shù)據(jù)寫回主內(nèi)存的時候,回立刻通知其他線程,這就是線程的可見性。先看一個簡單的例子:

class MyDataDemo {
    int num = 0;

    public void updateNum() {
        this.num = 60;
    }
}

public class VolatileDemo {

    public static void main(String[] args) {

        MyDataDemo myData = new MyDataDemo();

        new Thread(() -> {
            try {
                TimeUnit.SECONDS.sleep(3);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            myData.updateNum();
            System.out.println("num的值:" + myData.num);
        }, "子線程").start();

        while (myData.num == 0) {}
        System.out.println("程序執(zhí)行結(jié)束");
    }
}

這是一個簡單的示例程序,存在一個兩個線程,一個子線程修改主內(nèi)存的共享數(shù)據(jù)num的值,main線程使用while時時檢測自己是否是道主內(nèi)存的num的值是否被改變,運行程序程序執(zhí)行結(jié)束并不會被打印,同時,程序也不會停止。這就是線程之間的不可見問題,解決方法就是可以添加volatile關(guān)鍵字,修改如下:

volatile int num = 0;

1.2.2.Volatile保證可見性的原理

將Java程序生成匯編代碼的時候,我們可以看見,當我們對添加了volatile關(guān)鍵字修飾的變量時候,會多出一條Lock前綴的的指令。我們知道的是cpu不直接與主內(nèi)存進行數(shù)據(jù)交換,中間存在一個高速緩存區(qū)域,通常是一級緩存、二級緩存和三級緩存,而添加了volatile關(guān)鍵字進行操作時候,生成的Lock前綴的匯編指令主要有以下兩個作用:

  • 將當前處理器緩存行的數(shù)據(jù)寫回系統(tǒng)內(nèi)存;
  • 這個寫回內(nèi)存的操作會使得其他CPU里緩存了該內(nèi)存地址的數(shù)據(jù)無效;

Idea查看程序的匯編指令在VM啟動參數(shù)配上-XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly即可;

在多處理器下,為了保證各個處理器的緩存是一致的,就會實現(xiàn)緩存一致性協(xié)議,每個處理器通過嗅探在總線上傳播的數(shù)據(jù)來檢查自己緩存的值是不是過期了,當處理器發(fā)現(xiàn)自己緩存行對應(yīng)的內(nèi)存地址被修改,就會將當前處理器的緩存行設(shè)置成無效狀態(tài),當處理器對這個數(shù)據(jù)進行修改操作的時候,會重新從系統(tǒng)內(nèi)存中把數(shù)據(jù)讀到處理器緩存里。

總結(jié):Volatile通過緩存一致性保證可見性。

1.2.3.Volatile不保證原子性

原子性:也可以說是保持數(shù)據(jù)的完整一致性,也就是說當某一個線程操作每一個業(yè)務(wù)的時候,不能被其他線程打斷,不可以被分割操作,即整體一致性,要么同時成功,要么同時失敗。

class MyDataDemo {
    volatile int num = 0;

    public void addNum() {
        num++;
    }
}
public class VolatileDemo {

    public static void main(String[] args) {
        MyDataDemo data = new MyDataDemo();
        for(int i = 1; i <= 20; i++) {
            new Thread(() -> {
                for (int j = 1; j < 1000; j++) {
                    data.addNum();
                }
            }, "當前子線程為線程" + String.valueOf(i)).start();
        }
        // 等待所有線程執(zhí)行結(jié)束
        while (Thread.activeCount() > 2) {
            Thread.yield();
        }
        System.out.println("最終結(jié)果:" + data.num);
    }
}

上述代碼就是在共享數(shù)據(jù)前添加了volatile關(guān)鍵字,當時,打印的最終結(jié)果幾乎很難為20000,這就很充分的說明了volatile并不能保證數(shù)據(jù)的原子性,這里的num++操作,雖然只有一行代碼,但是實際是三步操作,這也是為什么i++在多線程下是非線程安全的。

1.2.4.為什么Volatile不保證原子性

可以參考JMM模型的那一張圖,就是主內(nèi)存中存在一個num = 0,當其中一個線程將其修改為1,然后將其寫回主內(nèi)存的時候,就被掛起了,另外一個線程也將主內(nèi)存的num = 0修改為1,然后寫入后,之前的線程被喚醒,快速的寫入主內(nèi)存,覆蓋了已經(jīng)寫入的1,造成了數(shù)據(jù)丟失操作,兩次操作最終結(jié)果應(yīng)該為2,但是為1,這就是為什么會造成數(shù)據(jù)丟失。再來看i++對應(yīng)的字節(jié)碼


簡單翻譯一下字節(jié)碼的操作:

  • aload_0:從局部變量表的相應(yīng)位置裝載一個對象引用到操作數(shù)棧的棧頂;
  • dup:復(fù)制棧頂元素;
  • getfield:先獲得原始值;
  • iadd:進行+1操作;
  • putfield:再把累加后的值寫回主內(nèi)存操作;

1.2.5.解決Volatile不保證原子性的問題

使用AtomicInteger來保證原子性,有關(guān)AtomicInteger的詳細知識,后面在死磕,官方文檔截圖如下:

修改之前的不保證原子性的代碼如下:

class MyDataDemo {
    AtomicInteger atomicInteger = new AtomicInteger();

    public void addAtomicInteger() {
        atomicInteger.getAndIncrement();
    }
}
public class VolatileDemo {

    public static void main(String[] args) {
        MyDataDemo data = new MyDataDemo();
        for(int i = 1; i <= 20; i++) {
            new Thread(() -> {
                for (int j = 1; j <= 1000; j++) {
                    data.addAtomicInteger();
                }
            }, "當前子線程為線程" + String.valueOf(i)).start();
        }
        // 等待所有線程執(zhí)行結(jié)束
        while (Thread.activeCount() > 2) {
            Thread.yield();
        }
        System.out.println("最終結(jié)果:" + data.atomicInteger);
    }
}

1.2.6.Volatile的禁止指令重排序

在程序中,我們覺得是會依次順序執(zhí)行,但是在計算機在執(zhí)行程序的時候,為了提高性能,編譯器和和處理器通常會對指令進行指令重排序,可能執(zhí)行順序為:2—1—3—4,也可能是:1—3—2—4,一般分為下面三種:


雖然處理器會對指令進行重排,但是同時也會遵守一些規(guī)則,例如上述代碼不可能重排后將第四句代碼第一個執(zhí)行,所以,單線程下確保程序的最終執(zhí)行結(jié)果和順序執(zhí)行結(jié)一致,這就是處理器在進行指令重排序時候必須考慮的就是指令之間的數(shù)據(jù)依賴性。
但是,在多線程環(huán)境下,由于編譯器重排的存在,兩個線程使用的變量能否保證一致性無法確定,所以結(jié)果就無法一致。在看一個示例:


在多線程環(huán)境下,第一種就是順序執(zhí)行init方法,先將num進行賦值操作,在執(zhí)行update方法,結(jié)果:num為6,但是存在編譯器重排,那么可能先執(zhí)行falg = true;再執(zhí)行num = 1;,最終num為5;

1.2.7.Volatile禁止指令重排序的原理

前面說到了volatile禁止指令重排優(yōu)化,從而避免在多線程環(huán)境下出現(xiàn)結(jié)果錯亂的現(xiàn)象。這是因為在volatile會在指令之間插入一條內(nèi)存屏障指令,通過內(nèi)存屏障指令告訴CPU和編譯器不管什么指令,都不進行指令重新排序。也就說說通過插入的內(nèi)存屏障禁止在內(nèi)存屏障前后的指令執(zhí)行指令重新排序優(yōu)化。

什么是內(nèi)存屏障

內(nèi)存屏障是一個CPU指令,他的作用有兩個:

  • 保證特定操作的執(zhí)行順序;
  • 保證某些變量的內(nèi)存可見性;

將上述代碼修改為:

volatile int num = 0;

volatile boolean falg = false;

這樣就保證執(zhí)行init方法的時候一定是先執(zhí)行num = 1;再執(zhí)行falg = true;,就避免的了結(jié)果出錯的現(xiàn)象。

1.3.Volatile的單例模式

public class SingletonDemo {

    private static volatile SingletonDemo instance = null;

    private SingletonDemo(){};

    public static SingletonDemo getInstance() {
        if (instance == null) {
            synchronized (SingletonDemo.class) {
                if (instance == null) {
                    instance = new SingletonDemo();
                }
            }
        }
        return instance;
    }
}
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容