volatile可見性的一些認識和論證


一、前言

    volatile的關鍵詞的使用在JVM內存模型中已是老生常談了,這篇文章主要結合自己對可見性的一些認識和一些直觀的例子來談談volatile。文章正文大致分為三部分,首先會介紹一下happen-before,接着講解volatile的一些使用場景,最后會附上一些例子來論證使用與不使用volatile的區別。

 

 

二、happen-before

    對操作系統有認識的同學一定知道,CPU一般有三級緩存,在與內存交互的時候,存在緩存與內存的更新問題,其次CPU在讀取指令的時候,會做一些指令重排序的工作,提高程序運行效率。類比JVM內存模型(見下圖),每個線程擁有自己的工作內存,同時存在一個主存,線程間通過主存來進行通信,同樣的,JVM也存在指令重排序,可見JVM內存模型與實際物理內存模型十分相似。(這里順便提一下,編譯器其實也會作一定重排序優化)。

    

    作為開發人員,你不可能了解到每個JVM優化細節,更不可能了解到CPU何時會進行指令重排序,所以java語言定義了更上層的一個概念,就是"happen-before"。起初,我看到這個單詞的時候,誤以為這是一個指令執行順序的規則,后來仔細想想又發覺不對勁。如果”happen-before“僅僅是抽象了指令執行順序的概念,那么它就把握不了“工作內存將值寫回主存”和“工作內存從主存中刷新自己的值”這個兩個action的時機。那么這個概念也就變得沒什么意義了。所以!所以!所以!”happen-before“是一個可見性的原則!!!

下面給出happen-before的具體規則:

程序次序規則:一個線程內,按照代碼順序,書寫在前面的操作先行發生於書寫在后面的操作;
鎖定規則:一個unLock操作先行發生於后面對同一個鎖額lock操作;
volatile變量規則:對一個變量的寫操作先行發生於后面對這個變量的讀操作;
傳遞規則:如果操作A先行發生於操作B,而操作B又先行發生於操作C,則可以得出操作A先行發生於操作C;
線程啟動規則:Thread對象的start()方法先行發生於此線程的每個一個動作;
線程中斷規則:對線程interrupt()方法的調用先行發生於被中斷線程的代碼檢測到中斷事件的發生;
線程終結規則:線程中所有的操作都先行發生於線程的終止檢測,我們可以通過Thread.join()方法結束、Thread.isAlive()的返回值手段檢測到線程已經終止執行;
對象終結規則:一個對象的初始化完成先行發生於他的finalize()方法的開始;

 

    

三、volatile的使用場景

    happen-before的第三條規則提到“volatile變量規則:對一個變量的寫操作先行發生於后面對這個變量的讀操作”,也就是說;一個volatile變量的寫操作對后續對讀操作可見。說白了就是每次寫完volatile變量,都會將值從工作內存寫回到主存中去,每次讀取volatile變量,工作內存必須從主存中刷新下自己的值。如此的話,volatile就是為了解決多個線程共享數據的可見性問題。但是不是任何數據共享場景都可以使用volatile,必須滿足以下兩種情景才行。

    應用場景:

    1.多個線程不依賴原值的情況下進行讀寫操作

    2.一個線程依賴原值進行寫操作,多個線程進行讀操作

在我看來,除了這兩種情況外,無非是多個線程依賴原值進行運算,這樣子倒不是說volatile可見性不起作用了,而是無法保證讀取原值和運算是一個原子操作!舉個簡單的例子,多個線程執行i++;i是一個共享變量,由於讀取i的值和i自增不是一個原子操作,所以i最終會丟失掉一部分自增過程。代碼如下,最終i輸出的結果是一個小於1000的整數。

/**
 * Created by chenqimiao on 17/8/23.
 */
public class Testv {
    public static volatile int i = 0;
    public static void main(String args[]){
        for (int i =0;i<1000;i++){
            new Thread(){
                public void run(){
                    Thread.yield();
                    Testv.i++;
                }
            }.start();
        }
        System.out.println(Testv.i);

    }
}

要滿足以上這種需求,我們還必須賦予代碼原子性,最常用的肯定是鎖操作了,一個字穩,性能可觀,同時保證原子性和可見性。如果想操作一波的話,還可以考慮使用一些無鎖操作,如CAS,象java.util.concurrent包下的一些原子類就是利用了CAS來做到原子性,但原子性並不能保證可見性,這個時候,還需要配合volatile。

以上種種都是對volatile使用場景的概括,想了解具體的使用場景可以參考博文:https://www.ibm.com/developerworks/cn/java/j-jtp06197.html

 

四、volatile可見性的證明

   先上段代碼好了,不知道從何說起了。

  

package com.example.demo.netty;

/**
 * Created with IntelliJ IDEA.
 * User: chenqimiao
 * Date: 2017/8/23
 * Time: 9:16
 * To change this template use File | Settings | File Templates.
 */
public class VolatileTest {

     boolean isStop = false;

    public void test(){
        Thread t1 = new Thread(){
            public void run() {
                isStop=true;
            }
        };
        Thread t2 =  new Thread(){
            public void run() {
                while (!isStop);
            }
        };

        t2.start();
        t1.start();

    }
    public static void main(String args[]) throws InterruptedException {
        for (int i =0;i<25;i++){
            new VolatileTest().test();
        }

    }
}

    上面這段代碼可能永遠也不會結束,因為線程一對isStop的賦值,線程二可能對此並不可見。當然只是可能,所以為了放大可見性問題,我這里作了25次循環。只要有一組線程,“線程一對isStop的賦值,線程二對此不可見”的情況發生,就不會退出程序。

    now,假如你給 isStop 添加一個 volatile 關鍵字,那么你會發現程序立馬就會退出。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM