java多線程——volatile

這是java多線程第三篇:java

《java 多線程—線程怎麼來的》安全

《java多線程-內存模型》多線程

上一篇《java多線程—內存模型》已經講解了java線程中三特徵以及happens-before 原則,這一篇主要講解一下volatile的原理以及應用,想必看完這一篇以後,你會對volatile的應用原理以及使用邊界會有更深入的認知。本篇主要內容:app

  1. volatile 讀寫同步原理
  2. volatile重排序原則
  3. volatile應用

 

關鍵字volatile是jvm提供的輕量級的同步機制,但它並不容易理解,並且在多數狀況下用不到,被多數開發者拋棄並採用synchronized代替,synchronized屬於重度鎖,若是你對性能有高的要求,那麼同等狀況下,變量聲明volatile會減少更少的同步開銷。jvm


在介紹以前,咱們先拋出2個問題:性能

一、volatile到底是如何保證共享變量的同步的?優化

二、i++操做爲什麼對虛擬機來講不是原子操做?this

 

1、volatile 讀寫同步原理

 

對變量進行volatile聲明之後,會有如下特徵:spa

一、可見性。  保證此變量對全部線程是可見的。.net

二、原子性 。只對任意單個volatile變量的讀/寫具備原子性(注意不是全部)。

三、有序性。被volatile聲明過的變量會禁止指令重排序優化

 

happen-before 保證可見性

volatile變量的寫-讀能夠實現線程之間的通訊。happens-before是java內存模型向咱們提供的內存可見性保證,這也就是咱們第一個問題的解答,volatiel如何保證對共享變量同步的。

咱們先回憶一下happens-before原則(咱們只說和其相關的):

程序次序法則:若是在程序中,全部動做 A 出如今動做 B 以前,則線程中的每動做 A 都 happens-before 於該線程中的每個動做 B。


Volatile 變量法則:對 Volatile 域的寫入操做 happens-before 於每一個後續對同一 Volatile 的讀操做。


傳遞性:若是 A happens-before 於 B,且 B happens-before C,則 A happens-before C。


咱們經過一個示例來講明這些規則的應用:

public class VolatileTest {

    private int a =0;

    private volatile int b=0;


    public void write(){

        a = 1;          //1

        b = 2;          //2

    }

    public void read(){

        int i = b;          //3

        int j = a;          //4

    }

}

好比如今有線程A和B,分別調取write和read方法

 

第一種狀況:

線程A先執行write方法以後,線程B執行read方法。那麼:

一、基於程序次序法則。1 happens-before 2; 3 happens-before 4

二、基於volatile原則。2 happens-before 3;

三、基於傳遞性原則。由於 1 happens-before 2,2 happens-before 3,3 happens-before 4。那麼能夠推斷出 1 happens-before 4,2 happens-before 4。

此種狀況下,咱們能夠認定此時線程B中能夠讀取到 線程A中寫入的 a和b的值的。(a值沒用聲明volatile依然能夠讀取到,這個爲什麼咱們後面講)

 

第二種狀況:

線程B先執行read方法,以後線程A執行write方法。

一、基於程序次序法則。3 happens-before 4; 1 happens-before 2

二、基於volatile原則。無;

三、基於傳遞性原則。無傳遞;

此種狀況下,咱們能夠此時認定線程B中沒有讀取到線程A中寫入的a和b的值。

 

經過上面的分析咱們能夠對volatiel變量如此定義:

  •  當write一個volatile變量時,JMM會把該線程對應的本地內存中的共享變量刷新到主內存。
  •  當read一個volatile變量時,JMM會把該線程對應的本地內存置爲無效。線程接下來將從主內存中讀取共享變量。


對於第一種狀況,咱們看上述示例如何write和read的:

那麼讀到這裏,有一個困惑:上述變量a並無聲明爲volatile ,爲什麼能被刷新到主內存中,難道不會被處理器重排序麼?

 

2、volatile限制重排序

 

上述中咱們講到volatile 中有一個特性,有序性,防止jvm對其重排序,那麼到底是如何作的,咱們看一下。

重排序分爲編譯器重排序和處理器重排序。爲了實現volatile內存語義,jvm會分別限制這兩種類型的重排序類型。


編譯器重排序

針對編譯器制定的volatile重排序規則:

第一個操做

第二個操做

 

普通讀/寫

volatile讀

volatile寫

普通讀/寫

 

 

NO

volatile讀

NO

NO

NO

volatile寫

 

NO

NO

上述表中,NO表示jvm不能夠重排序,保持當前順序

好比第一行第三列中表示:第一個操做是變量的普通讀寫,第二個操做是volatile聲明的變量寫操做,那麼此時對於操做1和操做2是不能夠重排序的,保持當前順序。

就比如上述示例中a 和b變量,知足此種狀況,a和b的操做順序不變。

上述規則用文字描述:

  • 當第二個操做是volatile寫時,無論第一個操做是什麼,都不能重排序。這個規則確保volatile寫以前的操做不會被編譯器重排序到volatile寫以後。
  • 當第一個操做是volatile讀時,無論第二個操做是什麼,都不能重排序。這個規則確保volatile讀以後的操做不會被編譯器重排序到volatile讀以前。
  • 當第一個操做是volatile寫,第二個操做是volatile讀時,不能重排序。


注意,jvm只保證2個操做保持如此規則,不能延伸到2個以上的操做上

 

處理器重排序

爲了實現上述規則,jvm編譯器在生成字節碼的時候,會在指令序列中插入內存屏障來禁止特定類型的處理器重排序。

在每一個volatile寫操做的前面插入一個StoreStore屏障。
在每一個volatile寫操做的後面插入一個StoreLoad屏障。
在每一個volatile讀操做的前面插入一個LoadLoad屏障。
在每一個volatile讀操做的後面插入一個LoadStore屏障。

如此能夠保證在任意處理器平臺,任意的程序中都能獲得正確的volatile重排序規則實現。


總結

volatile防止重排序,有什麼做用?

happens-before是java內存模型向咱們提供的內存可見性保證;而volatile的禁止重排序規則,包括volatile的編譯器重排序規則和volatile的內存屏障插入策略,是jvm用來實現happens-before的方式。

好比上述程序中,根據happens-before的程序順序規則:1 happens-before 2 ;3 happens-before 4.

然後根據volatile規則:2 happens-before 3. 如此操做 一、二、三、4的順序得以延續。

也就是說volatile的禁止重排序規則,確保上述happens-before順序。

 

3、應用

i++ 不是原子

上述原理介紹中,咱們有說volatile只對只對任意單個volatile變量的讀/寫具備原子性,好比變量a的賦值操做,能夠爲原子的,但變量a++不爲原子的,咱們看個示例:

public class Test {

    private volatile  int count;

    public void increCount(){

        count++;
    }

    public void setCount(int count ){

        this.count=count;
    }

}

咱們用javap 看下increCount的編譯指令:

我看紅色圈中的部分,increCount被分解了4個指令來操做,而 setCount只有1個指令來處理(原子的)。咱們用代碼的方式,increCount方法能夠等價於如下:

    public void increCount(){

//        count++;
        int tmp =getCount();
        tmp=tmp+1;
        setCount(tmp);
    }

因此說volatile只對只對任意單個volatile變量的讀/寫具備原子性,而i++實際上它是一個由讀取-修改-寫入操做序列組成的組合操做,屬於多個操做,因此不具有原子性。

 

volatile 應用原則

要使 volatile 變量提供理想的線程安全,必須同時知足下面兩個條件:

  • 對變量的寫操做不依賴於當前值。
  • 該變量沒有包含在具備其餘變量的不變式中。


也就是說被寫入 volatile 變量的這些有效值獨立於任何程序的狀態,包括變量的當前狀態。

所以只有在狀態真正獨立於程序內其餘內容時才能使用 volatile —— 這條規則可以避免將這些模式擴展到不安全的用例。

 

應用示例

 

一、賦值操做

上述 increCount中屬於依賴當前count值的應用了,而setCount屬於沒有依賴當前值。因此後者屬於線程安全。

 

二、線程取消

對一個線程取消或者中斷的時候,有人會採用interrupted方法來中斷,若是維護一個volatile變量來爲什麼,不管外部線程如何調用,總能保證對當前線程的當即可見性。

public class  CancleThread implements Runnable{

    private volatile  boolean cancle = false;

    public void shutdown(){

        this.cancle=true;
    }

   

    public void run() {

        while (!cancle){

            //.....doSomeThing
        }

    }

}

當想終止這個線程的操做的時候,調用shutdown方法會比較安全。

 

經過以上原理和應用介紹,想必對於volatile不會那麼陌生了,掌握原理,瞭解使用邊界,讓你的程序性能更高,可讀性更強。咱們若是嚴格遵循 volatile 的使用條件 —— 即變量真正獨立於其餘變量和本身之前的值 —— 在某些狀況下可使用 volatile 代替 synchronized 來簡化代碼。

 

-----------------------------------------------------------------------------

想看更多有趣原創的技術文章,掃描關注公衆號。

關注我的成長和遊戲研發,推進國內遊戲社區的成長與進步。

相關文章
相關標籤/搜索