死磕Java——volatile的理解

1、死磕Java——volatile的理解

1.1.JMM內存模型

理解volatile的相關知識前,先簡單的認識一下JMM(Java Memory Model),JMMjdk5引入的一種jvm的一種規範,自己是一種抽象的概念,並不真實存在,它屏蔽了各類硬件和操做系統的訪問差別,它的目的是爲了解決因爲多線程經過共享數據進行通訊時,存在的本地內存數據不一致、編譯器會對代碼進行指令重排等問題。java

JMM有關同步的規定:緩存

  • 線程解鎖前,必須把共享變量的值刷新回主內存;
  • 線程加鎖前,必須讀取主內存的最新值到本身的工做內存中;
  • 加鎖和解鎖使用的是同一把鎖;

關於上述規定以下圖解:安全

image-20190502153724126

**說明:**當咱們在程序中new一個user對象的時候,這個對象就存在咱們的主內存中,當多個線程操做主內存的name變量的時候,會先將user對象中的name屬性進行拷貝一份到本身線程的工做內存中,本身修改本身工做內存中的屬性後,再將修改後的屬性值刷新回主內存,這就會存在一些問題,例如,一個線程寫完,尚未寫回到主內存,另外一個線程先修改後寫入到主內存,就會存在數據的丟失或者髒數據。因此,JMM就存在以下規定:多線程

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

1.2.Volatile關鍵字

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

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

1.2.1.Volatile如何保證可見性

可見性就是當多個線程操做主內存的共享數據的時候,當其中一個線程修改了數據寫回主內存的時候,回馬上通知其餘線程,這就是線程的可見性。先看一個簡單的例子:性能

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("程序執行結束");
    }
}
複製代碼

這是一個簡單的示例程序,存在一個兩個線程,一個子線程修改主內存的共享數據num的值,main線程使用while時時檢測本身是不是道主內存的num的值是否被改變,運行程序程序執行結束並不會被打印,同時,程序也不會中止。這就是線程之間的不可見問題,解決方法就是能夠添加volatile關鍵字,修改以下:優化

volatile int num = 0;
複製代碼

1.2.2.Volatile保證可見性的原理

Java程序生成彙編代碼的時候,咱們能夠看見,當咱們對添加了volatile關鍵字修飾的變量時候,會多出一條Lock前綴的的指令。咱們知道的是cpu不直接與主內存進行數據交換,中間存在一個高速緩存區域,一般是一級緩存、二級緩存和三級緩存,而添加了volatile關鍵字進行操做時候,生成的Lock前綴的彙編指令主要有如下兩個做用:this

  • 將當前處理器緩存行的數據寫回系統內存;
  • 這個寫回內存的操做會使得其餘CPU裏緩存了該內存地址的數據無效;

Idea查看程序的彙編指令在VM啓動參數配上-XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly便可;atom

參考:wiki.openjdk.java.net/display/Hot…spa

在多處理器下,爲了保證各個處理器的緩存是一致的,就會實現緩存一致性協議,每一個處理器經過嗅探在總線上傳播的數據來檢查本身緩存的值是否是過時了,當處理器發現本身緩存行對應的內存地址被修改,就會將當前處理器的緩存行設置成無效狀態,當處理器對這個數據進行修改操做的時候,會從新從系統內存中把數據讀處處理器緩存裏。

總結:Volatile經過緩存一致性保證可見性。

1.2.3.Volatile不保證原子性

**原子性:**也能夠說是保持數據的完整一致性,也就是說當某一個線程操做每個業務的時候,不能被其餘線程打斷,不能夠被分割操做,即總體一致性,要麼同時成功,要麼同時失敗。

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();
        }
        // 等待全部線程執行結束
        while (Thread.activeCount() > 2) {
            Thread.yield();
        }
        System.out.println("最終結果:" + data.num);
    }
}
複製代碼

上述代碼就是在共享數據前添加了volatile關鍵字,當時,打印的最終結果幾乎很難爲20000,這就很充分的說明了volatile並不能保證數據的原子性,這裏的num++操做,雖然只有一行代碼,可是實際是三步操做,這也是爲何i++在多線程下是非線程安全的。

1.2.4.爲何Volatile不保證原子性

能夠參考JMM模型的那一張圖,就是主內存中存在一個num = 0,當其中一個線程將其修改成1,而後將其寫回主內存的時候,就被掛起了,另一個線程也將主內存的num = 0修改成1,而後寫入後,以前的線程被喚醒,快速的寫入主內存,覆蓋了已經寫入的1,形成了數據丟失操做,兩次操做最終結果應該爲2,可是爲1,這就是爲何會形成數據丟失。再來看i++對應的字節碼

image-20190502175617528

簡單翻譯一下字節碼的操做:

  • aload_0:從局部變量表的相應位置裝載一個對象引用到操做數棧的棧頂;
  • dup:複製棧頂元素;
  • getfield:先得到原始值;
  • iadd:進行+1操做;
  • putfield:再把累加後的值寫回主內存操做;

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

使用AtomicInteger來保證原子性,有關AtomicInteger的詳細知識,後面在死磕,官方文檔截圖以下:

image-20190502182016318

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

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();
        }
        // 等待全部線程執行結束
        while (Thread.activeCount() > 2) {
            Thread.yield();
        }
        System.out.println("最終結果:" + data.atomicInteger);
    }
}
複製代碼

1.2.6.Volatile的禁止指令重排序

首先,假如寫了以下代碼

carbon

在程序中,咱們以爲是會依次順序執行,可是在計算機在執行程序的時候,爲了提升性能,編譯器和和處理器一般會對指令進行指令重排序,可能執行順序爲:2—1—3—4,也多是:1—3—2—4,通常分爲下面三種:

image-20190502184808400

雖然處理器會對指令進行重排,可是同時也會遵照一些規則,例如上述代碼不可能重排後將第四句代碼第一個執行,因此,單線程下確保程序的最終執行結果和順序執行結一致,這就是處理器在進行指令重排序時候必須考慮的就是指令之間的數據依賴性

可是,在多線程環境下,因爲編譯器重排的存在,兩個線程使用的變量可否保證一致性沒法肯定,因此結果就沒法一致。在看一個示例:

http://image.luokangyuan.com/2019-05-02-113323.png

在多線程環境下,第一種就是順序執行init方法,先將num進行賦值操做,在執行update方法,結果:num爲6,可是存在編譯器重排,那麼可能先執行falg = true;再執行num = 1;,最終num爲5;

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

前面說到了volatile禁止指令重排優化,從而避免在多線程環境下出現結果錯亂的現象。這是由於在volatile會在指令之間插入一條內存屏障指令,經過內存屏障指令告訴CPU和編譯器無論什麼指令,都不進行指令從新排序。也就說說經過插入的內存屏障禁止在內存屏障先後的指令執行指令從新排序優化

什麼是內存屏障

內存屏障是一個CPU指令,他的做用有兩個:

  • 保證特定操做的執行順序;
  • 保證某些變量的內存可見性;

將上述代碼修改成:

volatile int num = 0;

volatile boolean falg = false;
複製代碼

這樣就保證執行init方法的時候必定是先執行num = 1;再執行falg = true;,就避免的告終果出錯的現象。

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;
    }
}
複製代碼
相關文章
相關標籤/搜索