Java多線程系列之volatile關鍵字

    在Java併發編程中synchronized和volatile是使用較爲頻繁的兩個關鍵字,咱們知道synchronized保證了在同一時刻多線程中只有一個線程能夠獲取到鎖執行同步代碼塊,而volatile比起synchronized更加輕量級,被volatile修飾的變量具備多線程修改可見性,當多線程訪問被volatile修飾的共享變量有一個線程修改了該變量其餘線程能夠當即察覺到變量修改,獲取到修改後的新值。java

    在理解volatile關鍵字以前咱們須要先從硬件層面瞭解下CPU、高速緩存和主存的關係。編程

 

1 - CPU、高速緩存和主存

    計算機的硬件組成主要有總線、IO設備、主存和CPU等,數據主要存放在主存中,CPU負責執行指令,CPU中大部分指令執行只須要幾個時鐘週期,而主存中數據讀寫一般須要幾十甚至上百個時鐘週期,因爲CPU的指令的執行速度遠高於主存中的數據讀取,若是從CPU中直接讀取主存中的數據進行處理中間會有很長時間的浪費,爲了平衡這種差別因而出現了高速緩存。高速緩存其實就是在CPU和主存之間開闢一片空間,在程序指令執行過程當中先將須要操做的主存中的數據複製一份到高速緩存中,CPU執行指令時直接從它的高速緩存中讀取數據,在程序結束時將數據會寫到主存中,這樣就有效下降了由於CPU執行指令和CPU從主存讀取數據的時間延遲。可是這樣作又會產生新的問題,若是是在單核CPU中不會有問題可是在多核CPU中就會出現新的問題,示意圖以下:緩存

    這種硬件架構存在的問題是在多核CPU中當有多個線程去訪問主存中的同一個共享變量或數據,Core0和Core1分別會在各自的工做緩存中爲該變量建立副本,這時候若是沒作額外控制,當其中一個執行線程好比Core0修改了變量值,此時Core1不知道讀取的還是本身副本中的數據,就會去操做這個髒數據,這就是這種架構致使的緩衝數據不一致的問題。解決問題的方法有主要有兩種:1.由於高速緩存和主存的交互都會通過總線,不管是從主存中複製數據到高速緩存仍是從高速緩存將數據刷新到主存。咱們能夠直接對總線加鎖,但是這樣的話某一時刻就只能有一個線程對共享變量進行讀寫,實質就是把多核操做變成單核;2.經過緩存鎖即緩存一致性協議實現,在Core0操做一個變量時若是發現是共享變量當即發出信號通知其餘持有該變量副本的CPU如Core1將緩存中該變量副本設置爲失效,並將本身工做緩存中修改的共享變量當即刷新到主存,下次Core1會直接從主存中讀取共享變量。多線程

2 - Java內存模型

    Java爲了在虛擬機層面爲不一樣硬件和操做系統解決這種緩存不一致性問題提供一個統一的解決方案。它在JVM中從新定義了一個java內存模型(即JMM),模型設計如圖:架構

 

 

    Java內存模型定義了共享變量的訪問規則,共享變量存儲在主內存中,當多線程併發訪問共享變量時會爲每一個線程單獨分配一片工做內存,從主內存中複製一份共享變量到各自的工做內存中,在通常狀況下對共享變量的操做在工做內存中的變量副本上進行在線程執行結束以後將修改後的變量副本刷新到主內存中,線程間不能互相訪問對方的工做內存,所以線程方法內部對共享變量修改其餘線程是不可見的。使用volatile修飾的共享變量,多線程環境下有線程對它進行修改時會在工做內存中和主存中進行變量的同步更新,其餘線程讀取被volatile修飾的共享變量時會將工做內存中的變量副本設置爲失效直接從主內存中讀取該共享變量。此外虛擬機爲了提高性能會在編譯器和程序執行時進行指令重排序,這在單線程環境下沒有問題,但虛擬機進行指令重排序時只考慮存在顯式依賴關係變量的操做指令的前後順序保證最終操做結果與重排序以前的執行結果一致但在多線程環境下仍然可能產生錯誤的執行結果,例如分析下下面代碼:併發

package com.clpublic.factory.test;

public class VolatileTest {
    int x = 0;
    volatile  boolean flag = false;

    public void write() {
        x = 42;
        flag = true;
    }

    public void read() {
        if (flag == true) {
            System.out.println(x);
        }
    }

    public static  void main(String[] args){
        VolatileTest test = new VolatileTest();
        new Thread(new Runnable() {
            @Override
            public void run() {
                test.read();
            }
        }).start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                test.write();
            }
        }).start();
    }
}

    若是volatile未對編譯器和運行期的指令重排序作出限制,那麼程序的執行結果可能就不是42,考慮這樣一種狀況經指令重排以後read方法執行順序變成 flag = true->x = 42那麼此時write線程執行write方法時打印的值就是0,與程序本來的語義相悖。JVM在指令重排時保證了volatile共享變量的局部有序,volatile修飾的共享變量的全部操做指令不管讀寫,volatile操做指令前的全部指令在排序後沒法放在該指令後執行,volatile操做指令後的全部指令重排序後也不能放到該指令前執行。JVM內部經過在編譯器生成字節碼指令序列時插入內存屏障禁止特定規則的指令重排序。JVM爲了保證volatile的局部有序遵循如下幾條規則:ide

1.volatile變量的寫操做要優先於對volatile變量的讀操做;性能

2.轉遞性原則,若是A操做先於B操做,B操做先於C操做,那麼A操做確定先於C操做。spa

相關文章
相關標籤/搜索