# 深刻理解volatile

深刻理解volatile

Volatile的官方定義

Java語言規範第三版中對volatile的定義以下:java編程語言容許線程訪問共享變量,爲了確保共享變量能被準確和一致的更新,線程應該確保經過排他鎖單獨得到這個變量。Java語言提供了volatile,在某些狀況下比鎖更加方便。若是一個字段被聲明成volatile,java線程內存模型確保全部線程看到這個變量的值是一致的。
volatile變量修飾符若是使用恰當的話,它比synchronized的使用和執行成本會更低,由於它不會引發線程上下文的切換和調度。java

可見性

處理器爲了提升處理速度,不直接和內存進行通信,而是先將系統內存的數據讀到內部緩存L1,L2或其餘)後再進行操做,但操做完以後不知道什麼時候會寫到內存,若是對聲明瞭Volatile變量進行寫操做,JVM就會向處理器發送一條Lock前綴的指令,將這個變量所在緩存行的數據寫回到系統內存。可是就算寫回到內存,若是其餘處理器緩存的值仍是舊的,再執行計算操做就會有問題,因此在多處理器下,爲了保證各個處理器的緩存是一致的,就會實現緩存一致性協議,每一個處理器經過嗅探在總線上傳播的數據來檢查本身緩存的值是否是過時了,當處理器發現本身緩存行對應的內存地址被修改,就會將當前處理器的緩存行設置成無效狀態,當處理器要對這個數據進行修改操做的時候,會強制從新從系統內存裏把數據讀處處理器緩存裏。c++

緩存一致性解決方案

數據總線加鎖

LOCK前綴指令會引發緩存回寫到內存,LOCK前綴指令致使執行指令期間,聲明處理器的LOCK#信號。在多處理器環境中,LOCK# 信號確保在聲言該信號期間,處理器能夠獨佔使用任何共享內存。(由於它會鎖住總線,致使其餘CPU不能訪問總線,不能訪問總線就意味着不能訪問系統內存)程序員

緩存一致性協議

LOCK#信號通常不鎖總線,而是鎖緩存,畢竟鎖總線開銷比較大。對於Intel486和Pentium處理器,在鎖操做時,老是在總線上聲言LOCK#信號。但在P6和最近的處理器中,若是訪問的內存區域已經緩存在處理器內部,則不會聲言LOCK#信號。相反地,它會鎖定這塊內存區域的緩存並回寫到內存,並使用緩存一致性機制來確保修改的原子性,此操做被稱爲「緩存鎖定」,緩存一致性機制會阻止同時修改被兩個以上處理器緩存的內存區域數據 。
一個處理器的緩存回寫到內存會致使其餘處理器的緩存無效 。IA-32處理器和Intel 64處理器使用MESI(修改,獨佔,共享,無效)控制協議去維護內部緩存和其餘處理器緩存的一致性。在多核處理器系統中進行操做的時候,IA-32 和Intel 64處理器能嗅探其餘處理器訪問系統內存和它們的內部緩存。它們使用嗅探技術保證它的內部緩存,系統內存和其餘處理器的緩存的數據在總線上保持一致。例如在Pentium和P6 family處理器中,若是經過嗅探一個處理器來檢測其餘處理器打算寫內存地址,而這個地址當前處理共享狀態,那麼正在嗅探的處理器將無效它的緩存行,在下次訪問相同內存地址時,強制執行緩存行填充。編程

內存模型

在這裏插入圖片描述

代碼示列

public class VolatileTest {
    //可見性
    private /**volatile**/ static int INIT_VALUE = 0;

    private final static int MAX_VALUE = 5;

    public static void main(String[] args) throws InterruptedException {
        new Thread(() -> {
            int localVale = INIT_VALUE;
            while (localVale < MAX_VALUE) {
                /***  對INIT_VALUE沒有volatile關鍵字修  ***/
                //爲何這裏一直沒有去從主內存中拿數據進行刷新呢?
                //這是由於java認爲這裏沒有writer的操做,因此不須要去主內存中獲取新的數據。這個具體最新的值被刷新指
                //具體能夠VolatileTest2進行比較
                //在這裏加一個sysytem的輸出是有可能會去刷新主存的,或者每次運行的時候休眠一小段時間,
                // 這個程序是有可能會結束的。若是沒有System的輸出,或者休眠,在while判斷會一直不去主內存
                //刷新新數據,也就致使程序一直無法結束。
                //System.out.println("=");
                /*try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }*/
                if (localVale != INIT_VALUE) {
                    System.out.println("The value updated to [ " + INIT_VALUE + " ]");
                    localVale = INIT_VALUE;
                }
            }
        },"READER").start();
        TimeUnit.SECONDS.sleep(1);
        new Thread(() -> {
            int localValue = INIT_VALUE;
            while (INIT_VALUE < MAX_VALUE) {
                System.out.println("update the value to [ " + (++localValue) + " ]");
                INIT_VALUE = localValue;
                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"UPDATER").start();
    }
}

有序性

happens-before規則

Java的內存模型具有一些天生的有序規則,不須要任何同步手段就可以保證的有序性,這個規則被稱爲Happens-before原則,若是兩個操做的執行順序沒法從happens-before原則推導出來,那麼它們就沒法保證有序性,也就是說虛擬機或處理器能夠隨意對它們進行重排序處理。緩存

  1. 程序次序規則:在一個線程內,代碼按照編寫時的次序執行,編寫在後面的操做發生於編寫在前面的操做以後(若是這個都不能保證,咱們程序員還怎麼編程呢????對吧,因此這個確定是須要保證的)
    這個規則的意思就是程序按照編寫的順序來執行,可是虛擬機仍是可能會對程序代碼的指令進行重排序,只要確保在一個線程內最終的結果和代碼順序執行的結果一致便可。
  2. 鎖定規則:一個unlock操做要先行發生於同一個鎖的lock操做
    這句話的意思是,不管是單線程仍是多線程的環境下,若是同一個鎖是鎖定狀態,那必須先對其執行釋放操做以後才能繼續執行lock操做。
  3. Volatile變量規則:對一個變量的寫操做要早於對這個變量以後的讀操做
    這句話的意思是,若是一個變量使用volatile關鍵字修飾,一個線程對它進行讀操做,一個線程對他進行寫操做,那麼寫入操做確定要先行於讀操做。
  4. 傳遞規則:若是操做A先於操做B,而操做B又先於操做C,則能夠得出操做A確定先於操做C。
  5. 線程啓動規則:Thread對象的start()方法先行發生於對該線程的任何動做,只有start以後線程才能真正運行,不然Thread也只是一個對象而已。
  6. 線程中斷規則:對線程執行interrupt()方法確定要優先於捕獲到中斷信號。若是線程收到了中斷信號,那麼在此以前勢必要有interrupt()。
  7. 線程終結規則:線程中全部的操做都要先行發生於線程的終止檢測,通俗的講,線程的任務執行、邏輯單元執行確定要發生於線程死亡以前。
  8. 對象終結規則:一個對象的初始化完成先行於finalize()方法以前。

指令重排序

由程序次序規則,在單線程的狀況下,對於指令重排序不會出現什麼問題,可是對於多線程的狀況下,就頗有可能會因爲指令重排序出現問題。
volatile關鍵字直接禁止JVM和處理器對volatile關鍵字修飾的指令重排序,可是對於volatile先後五以來的指令則能夠隨便怎麼排序多線程

int x = 10
int y = 20
/**
  在語句volatile int z = 20以前,先執行x的定義仍是先執行y的定義,咱們並不關心,只要可以百分百
  保證在執行到z=20的時候,x=0,y=1已經定義好,同理對於x的自增以及y的自增操做都必須在z=20之後才能發生,這個規則能夠認爲是由程序次序規則+volatile規則推導
**/
valatile int z = 20
x++;
y++;
private volatile boole init = false;
private Context context ;
public Context context() {
    if(!init){
        context = loadContext();
        /**
            若是init不使用volatile關鍵字修飾的話,因爲編譯器會對指令作必定的優化,也就是指令重排序。
            因此在由多線程執行的狀況下,如某個線程A它可能執行init = true,後執行context = loadContext(),
            由於這兩條指令並無任何的依賴關係,因此執行順序可能不定。當線程B執行到判斷的時候,發現init=true成立,
            那麼線程B就不會再去加載context啦,此時若是它使用context,有可能context在線程A中尚未加載成功,此時線程B去
            使用context就有可能報空指針異常。
            而volatile關鍵字能阻止指令重排序,也就是說在init=true以前必定保證context=loadContext()執行完畢。
        **/
        init = true; //阻止指令重排序
    }
}

其實被volatile修飾的變量存在一個「lock」的前綴。
lock前綴實際上至關因而一個內存屏障,該內存屏障會爲指令的執行提供以下幾個保障
1.確保指令重排序不會將其後面的代碼排到內存屏障以前
2.確保指令重排序不會將其前面的代碼拍到內存屏障以後。
3.確保在執行內存屏障修飾的指令時前面的代碼所有執行完成(1,2,3阻止了指令重排序)
4.強制將線程工做內存中的修改刷新至主內存中
5.若是是寫操做,則會致使其餘線程的工做內存(CPU Cache)中的緩存數據失效。(4,5保證了內存可見性)併發

原子性

volatile沒法保證原子性
原子性:一個操做或多個操做,要麼都成功,要麼都失敗,中間不能因爲任何的因素中斷
對基本數據類型的變量讀取和賦值是保證了原子性的,要麼都成功,要麼都失敗,這些操做不可被中斷
a = 10; 原子性
b = a; 不知足1.read a; 2.assign to b;
c++; 不知足1.read c; 2.add 3.assign to c
c = c + 1; 不知足1.read c; 2.add 3.assign to capp

public class VolatileTest2 {

    //雖然保證了可見性,可是沒有保證原子性
    private volatile static int INIT_VALUE = 0;

    private final static int MAX_VALUE = 50;

    public static void main(String[] args) {
        new Thread(() -> {
            while (INIT_VALUE < MAX_VALUE) {
                //頗有可能會輸出重複的數字
                System.out.println("ADD-1-> " + (++INIT_VALUE));
                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"ADD-1").start();

        new Thread(() -> {
            while (INIT_VALUE < MAX_VALUE) {
                System.out.println("ADD-2-> " + (++INIT_VALUE));
                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"ADD-2").start();

    }
}

volatile與synchronized

(1) 使用上的區別jvm

  • volatile只能用於修飾實例變量或類變量,不能用於修飾方法以及方法參數和局部變量,常量等。
  • synchronized關鍵字不能用於對變量的修飾,只能用於修飾方法或者語句塊。
  • volatile修飾的變量能夠爲null,synchronized關鍵字同步語句塊的moniter對象不能爲null。
    (2)對原子性的保證
  • volatile沒法保證原子性
  • 因爲synchronized是一種排他機制,所以synchronized關鍵字修飾的同步代碼是沒法被中斷,所以可以保證其原子性。
    (3)可見性的保證
    二者都可以保證共享資源在多線程間的可見性,可是實現機制徹底不一樣
    synchronized藉助於jvm指令的monitor enter和moniter exit對經過排他的方式使得同步代碼串行化,在monitor exit時全部共享資源都會被刷新到主存中;
    volatile使用機器指令(lock;)的方式迫使其餘線程工做內存的數據失效,不得不到主存中從新加載數據
    (4)對有序性的保證
    二者都保證有序性,volatile關鍵字禁止jvm編譯器以及處理器對其進行重排序,因此他可以保證有序性
    synchronized以程序的串行化執行來保證有序性

參考博客

併發之volatile底層原理:http://www.javashuo.com/article/p-fgsreuii-a.html

相關文章
相關標籤/搜索