對volatile的理解

JMM 是什麼

JMM(java內存模型 Java Memory Model)自己是一種抽象的概念,描述一組規則後規範經過這組規範定義了程序中各個變量(包括實例字段,靜態變量和組成數組對象的元素)的訪問方式。java

JMM關於同步的規定:數組

  1. 線程解鎖前,必須把共享變量的值刷新回主內存
  2. 線程加鎖前,必須讀取主內存的最新值到本身的工做內存
  3. 加解鎖是同一把鎖

因爲JMM運行程序的實體是線程,而每一個線程建立JVM都會爲其建立一個工做內存,工做內存是每一個線程的私有數據區域,而Java內存模型中規定全部變量都存儲在 主內存,主內存是共享內存區域,全部線程均可以訪問,但線程對變量的操做(讀取或賦值)必須在工做內存中進行,首先要將內存從主內存拷貝到本身的工做內存空間,而後對變量進行操做,操做完成後再將變量寫回到主內存,不能直接操做主內存中的變量,各個線程中的工做內存中存儲着主內存中的變量副本拷貝,所以不一樣的線程間沒法訪問對方的工做內存,線程間的通訊(值傳遞)必須經過主內存來完成。訪問過程以下:安全

JMM

JMM三個特性(約束)

  1. 可見性
  2. 原子性
  3. 有序性

volatile是什麼

volatile是Java虛擬機提供的輕量級同步機制多線程

  1. 保證可見性
  2. 不保證原子性
  3. 禁止指令重排

什麼是可見性

public class VolatileDemo {
    public static void main(String[] args) throws InterruptedException {
        MyData myData = new MyData();
        new Thread(() -> {
            try {
                TimeUnit.SECONDS.sleep(3);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            myData.add60();
            System.out.println(Thread.currentThread().getName()+ " 線程內修改num的值爲 :" + myData.number);
        }, "Thread1").start();

        while (myData.number == 0) {

        }
        TimeUnit.SECONDS.sleep(4);
        System.out.println(Thread.currentThread().getName()+ " 主線程從循環中跳出");
    }
}

class MyData {
    volatile int number = 0;

    void add60() {
        this.number = 60;
    }
}
複製代碼

以上代碼執行結果爲性能

Thread1  線程內修改num的值爲 :60
main   主線程從循環中跳出
複製代碼

由結果可知,線程之間是有可見性的,即線程Thread1 修改了number的值爲60,主線程的工做內存中的number讀到修改後的值爲60,便跳出循環,輸出循環外的語句。優化

若是將classData中的number去掉volatile的修飾,則線程間沒有可見性,即主線程讀不到number修改後的值,main的工做內存中仍是保留number值爲0,則一直停留在循環中。this

什麼是原子性

不可見分割,完整性,某個線程正在作某個具體業務時,中間不可被加塞或者分割,須要總體完成,要麼成功,要麼失敗。spa

public class VolatileAtomicity {
    public static void main(String[] args) {
        MyDatas myDatas = new MyDatas();
        for (int i = 0; i < 20; i++) {
            new Thread(() -> {
                for (int j = 0; j < 1000; j++) {
                    myDatas.numPlusPlus();
                }
            }).start();
        }
        while (Thread.activeCount() > 2) {
            Thread.yield();
        }
        System.out.println(myDatas.number);
    }
}

class MyDatas {
    volatile int number = 0;

    void numPlusPlus() {
        number++;
    }
}
複製代碼

以上代碼執行結果老是小於2000
首先咱們知道 number++ 操做是分兩步執行:線程

  1. 獲取當前number的值
  2. number++
  3. 結果放回主內存中。
    根據上述,volatile修飾的number是不保證原子性的,也就是在執行number++的過程會被其餘線程打斷,即A線程獲取number的值爲1時,同時B線程也獲取了number的值爲1,A進行第二步number+1,而後第三步準備將結果2放回主內存時,線程B搶先一步將其工做內存中number+1的結果2放回主內存,理論上A/B兩個線程各加了1,結果應該爲3,可是實際上A線程計算結果也是2,放回主內存時只是覆蓋了B線程的結果,最終主內存中的結果在通過兩個線程分別+1後仍是2.

以上,可知volatile是不符合JMM規定的原子性的,同時number++在多線程下是非線程安全的。code

如何解決原子性問題

  • 加synchronized
  • 使用原子類AtomicInteger
    使用synchronized對於此場景來講過重,所以優先考慮使用AtomicInteger做爲number的類型。

有序性

volatile的有序性表如今 禁止指令重排    
複製代碼

指令重排

計算機在執行程序時,爲了提升性能,編譯器和處理器經常會對指令作重排,通常分爲三類:

指令重排

單線程環境裏面確保程序最終執行結果和代碼順序執行的結果一致
處理器在進行重排序時必須考慮指令間的數據依賴性
多線程環境中線程交替執行,因爲編譯器優化重排的存在,兩個線程中使用的變量可否保證一致性是沒法肯定的,結果沒法預測

void mysort() {
        int x = 11;
        int y = 12;
        x = x + 5;
        y = x * x;
    }
複製代碼

以上代碼,執行順序 123四、213四、1324 是不影響最終的結果的,所以代碼編譯後指令順序不必定爲1234,這種行爲在多線程下可能會形成結果的偏差。

public class ReSoreSeqDemo {
    int a = 0;
    boolean flag = false;

    public void method1() {
        a = 1;
        flag = true;
    }

    public void method2() {
        if (flag) {
            a = a + 5;
            System.out.println("****value = " + a);
        }
    }
}
複製代碼

如上代碼,現有兩個線程A/B,分別執行method1和2,僅對於線程A來講,method1方法兩行執行順序前後沒有關係。可是若是method1順序變了,先執行flag=true,這時線程B就進入method2的判斷中,a=5,再接着執行線程A的 a=1,最後結果爲 a = 1,與指望結果 a=1 而後 a = a+5 的結果有差別。這就是須要禁止指令重排的緣由。

volatile實現禁止指令重排優化,從而避免多線程環境下環境出現亂序執行的現象。

內存屏障(Memory Barrier)

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

  1. 保證特定操做的執行順序
  2. 保證某些變量的內存可見性(利用該特性實現volatile的內存可見性)

因爲編譯器和處理器都能執行指令重排優化。若是在指令間插入一條MemoryBarrier則會告訴編譯器和CPU, 無論什麼指令都不能和這條MemoryBarrier指令重排序,也就是經過內存屏障禁止在內存屏障先後的指令執行重排序優化。內存屏障另外的一個做用是強制刷出各類CPU的煥醋拿數據,所以任何CPU上的線程都能讀取到這些數據的最新版本。

對volatile變量進行寫操做時,會在寫操做後加入一條store屏障指令,將工做內存中的共享變量之刷新到主內存中;
對volatile變量進行讀操做時,會在讀操做前加入一條load屏障指令,從主內存中讀取共享變量

線程安全如何得到保障

工做內存和主內存同步延遲現象致使的可見性問題

  • 可使用synchronized或volatile關鍵字解決,他們均可以是一個線程修改後的變量當即對其餘線程可見

對於指令重排致使的可見性問題和有序性問題

  • 能夠利用volatile關鍵字解決,由於volatile的另外一個做用就是禁止重排指令優化

本文結束
內容根據尚硅谷視頻總結
歡迎訪問個人我的博客 justd的博客

相關文章
相關標籤/搜索