volatile引起的一系列血案

最先讀《深刻理解java虛擬機》對於volatile部分就沒有讀明白,最近從新拿來研究並記錄一些理解java

理解volatile前須要把如下這些概念或內容理解:git

一、JMM內存模型編程

二、併發編程的三問題:原子性、一致性、有序性緩存

三、先行發生原則安全

而後咱們結合上面的幾個知識點來看volatile如何使用多線程

JMM內存模型

先看一下上面這張圖片,即Java內存模型規定全部的變量都是存在主存當中(相似於前面說的物理內存),每一個線程都有本身的工做內存(相似於前面的高速緩存)。線程對變量的全部操做都必須在工做內存中進行,而不能直接對主存進行操做。而且每一個線程不能訪問其餘線程的工做內存併發

那麼JMM爲什麼要如此設計?其主要緣由有兩點:一、達到各平臺訪問內存效果的一致性 二、提高數據訪問速度app

對於提高數據訪問速度,主要用到了CPU高速緩存這部份內容:計算機在執行程序時,每條指令都是在CPU中執行的,而執行指令過程當中,勢必涉及到數據的讀取和寫入。因爲程序運行過程當中的臨時數據是存放在主存(物理內存)當中的,這時就存在一個問題,因爲CPU執行速度很快,而從內存讀取數據和向內存寫入數據的過程跟CPU執行指令的速度比起來要慢的多,所以若是任什麼時候候對數據的操做都要經過和內存的交互來進行,會大大下降指令執行的速度。所以在CPU裏面就有了高速緩存jvm

在本文中,JMM可以幫助咱們理解爲何會發生可見性問題函數

併發編程的三問題:原子性、可見性、有序性

原子性問題

原子性指:一個操做執行時不能被打斷或插入

好比i++,JVM指令包括3個操做:讀取x的值,進行加1操做,寫入新的值,若是併發執行i++,可能這三步操做不一樣線程會穿插執行,原子性就是指,任何一個線程運行這三個操做時,其餘線程不能進入運行這三步操做

如何解決原子性問題:
一、synchronized 二、Lock、其餘鎖

可見性問題

每一個線程都有各自的工做內存(高速緩存、詳見JMM),線程A更改了變量的值後,線程B從本身的工做內存中獲取變量的值還多是A修改前的值

如何解決可見性問題:

一、volatile關鍵字 二、Lock、synchronized

有序性問題

先看下什麼是指令重排:處理器爲了提升程序運行效率,可能會對輸入代碼進行優化,它不保證程序中各個語句的執行前後順序同代碼中的順序一致,可是它會保證程序最終執行結果和代碼順序執行的結果是一致的,重排序過程不會影響到單線程程序的執行,卻會影響到多線程併發執行的正確性。若是程序不知足先行發生原則,那麼可能發生指令重排

指令重排就影響了程序的有序性

如何解決有序性問題
一、volatile關鍵字 二、Lock、synchronized

從上面的三個問題來看volatile只能解決:可見性問題、有序性問題,但沒法解決原子性問題,原子性問題仍須要鎖的手段才能解決

先行發生原則 Happens-Before

先行發生原則(Happens-Before)是判斷數據是否存在競爭、線程是否安全的主要依據,先行發生原則,能夠幫你斷定是否併發安全的,從而沒必要去猜想是不是線程安全了

下面是Java內存模型中一些「自然的」先行發生關係,這些先行發生關係無需任何同步協助器協做java自帶這些規則,能夠直接在編碼中使用。若是兩個關係不在此列,而又沒法經過這些關係推導出來,它們的順序就沒法保證,虛擬機能夠對它們任意重排序

程序次序規則: 同一個線程內,按照代碼出現的順序,前面的代碼 happens-before 後面的代碼,準確的說是控制流順序,由於要考慮到分支和循環結構。
管程鎖定規則: 對於一個監視器鎖的unLock操做 happens-before 於每一個後續對同一監視器鎖的Lock操做。
volatile變量規則: 對volatile域的寫入操做 happens-before 於每一個後續對同一個域的讀操做。
線程啓動規則: 在同一個線程裏,對Thread.start的調用會 happens-before 於每個啓動線程中的動做。
線程終結規則: 線程中的全部動做都 happens-before 於其它線程檢測到這個線程已經終結,或者從Thread.join()調用成功返回,或者Thread.isAlive返回false.
中斷規則: 一個線程調用另外一個線程的interrupt happens-before 於被中斷的線程發現中斷(經過拋出InterruptedException 或者調用isInterrupted和interrupted)
終結規則: 一個對象的構造函數的結束 happens-before 於這個對象finalizer的開始
傳遞性: 若是 A happens-before 於 B,且 B happens-before 於 C,則 A happens-before 於C。

其中比較重要且難以理解的幾條是:

程序次序規則

一段程序代碼的執行在單個線程中看起來是有序的。雖然這條規則中提到「書寫在前面的操做先行發生於書寫在後面的操做」,這個應該是程序看起來執行的順序是按照代碼順序執行的,由於虛擬機可能會對程序代碼進行指令重排序。雖然進行重排序,可是最終執行的結果是與程序順序執行的結果一致的,它只會對不存在數據依賴性的指令進行重排序。所以,在單個線程中,程序執行看起來是有序執行的,這一點要注意理解。事實上,這個規則是用來保證程序在單線程中執行結果的正確性,但沒法保證程序在多線程中執行的正確性

管程鎖定規則

一個unlock操做先行發生於後面(時間上)對同一個鎖的lock操做,也就是說不管在單線程中仍是多線程中,同一個鎖若是出於被鎖定的狀態,那麼必須先對鎖進行了釋放操做,後面才能繼續進行lock操做

volatile變量規則

對一個volatile變量的寫操做先行發生於後面(時間上)對這個變量的讀操做,若是線程1寫入了volatile變量v,接着線程2讀取了v,那麼,線程1寫入v及以前的寫操做都對線程2可見(線程1和線程2能夠是同一個線程),能夠當作是volatile解決可見性問題的描述

總結下來就是先行發生原則能夠肯定兩件事:

一、能幫助咱們判斷程序是否線程安全

二、能幫助咱們肯定程序是否可能發生指令重排

 使用volatile

有了以上知識儲備咱們來看一下volatile如何正確的使用

一、多讀單寫

只有一個線程控制改變volitile變量的值,一個或多個線程併發讀取volitile變量的值均可以用volitile

一般:線程開關或者狀態標記的場景可使用

由於可見性保證了volatile多讀單寫的能力,但又由於volatile沒有解決原子性問題的能力,因此不是多讀多寫

public static volatile boolean flag = false;
//這種狀況不添加volatile就有可能形成沒法退出程序了
//添加了volatile就強制從主內存中得到值,就不會出現上述問題了
//這個例子體現:只有一個線程控制改變volatile變量的值 不少線程併發讀取volitile變量的值均可以用volatile
new Thread(() -> {
    while(!flag){
    }
    System.out.println("退出了");
}).start();
try {
    Thread.sleep(3000);
} catch (InterruptedException e) {
    e.printStackTrace();
}
System.out.println("setup");
flag = true;
//特別說明:我測試flag是非volatile,當不在while(!flag){上sleep,會一直循環,這種很是可能拿不到更改後的值,一直從工做內容中得到緩存值false。

二、防止指令重排

防止指令重排,一般:單例懶漢模式 double-check中使用

public class LazySingleton {
    private volatile static LazySingleton lazySingleton = null;
    private LazySingleton(){
    }
    public static LazySingleton getInstance(){
        if(lazySingleton == null){
            synchronized (LazySingletonill.class){
                if (lazySingleton == null) {
                    lazySingleton = new LazySingleton();
                }
            }
        }
        return lazySingleton;
    }
    public static void main(String[] args){
        for (int i = 0; i < 100; i++) {
            new Thread(() -> {
                System.out.println(LazySingleton.getInstance().hashCode());
            }).start();
        }
    }
}

咱們來看一下爲何不加volatile會引起指令重排的問題:

首先,這個出現問題的機率並不高,而且我經過jdk8的版本反編譯並未和帖子內容一致,姑且先把帖子的原理寫一下:

instance = new LazySingleton();,其實JVM內部已經轉換爲多條指令:
memory = allocate(); //1:分配對象的內存空間
ctorInstance(memory); //2:初始化對象
instance = memory; //3:設置instance指向剛分配的內存地址

可是通過指令重排序後以下:

memory = allocate(); //1:分配對象的內存空間
instance = memory; //3:設置instance指向剛分配的內存地址,此時對象還沒被初始化
ctorInstance(memory); //2:初始化對象

二、3步驟指令重排後發生了交換

 假如線程A得到了鎖而且正在執行lazySingleton = new LazySingleton();,這個實例化的jvm指令發生了重排,即instance = memory先於ctorInstance(memory)執行,恰好instance = memory執行完畢,線程B登場在執行if(lazySingleton == null){時爲false,線程B return了一個沒有初始化對象的實例出去,出現了返回不正確結果的現象

 
發個牢騷:這個單例寫法真的太矯情了,另外這種懶漢模式double-check寫法演化問題分析詳見:
https://gitee.com/zxporz/zxp-thread-test/blob/master/src/main/java/org/zxp/thread/volatileTest/singleton/LazySingletonill.java
相關文章
相關標籤/搜索