volatile在多線程中的使用

volatile

volatile修飾的變量,線程每次使用變量時,都會讀取變量修改後的最新值,但volatile並不表示原子性操作,它只能保證該變量被修改後馬上更新到主存(即保證下一個要讀取的線程可以讀取到最新值),若是之前已經被其他線程讀取到線程的工作內存,那麼該變量是不會更新過去的,看如下程序:

public class volatileTest {
    public static int count = 0;

    public static void inc() {

        //這裏延遲1毫秒,使得結果明顯
        try {
            Thread.sleep(1);
        } catch (InterruptedException e) {
        }

        count++;
        System.out.println(Thread.currentThread().getName()+",運行結果:count=" + volatileTest.count);
    }

    public static void main(String[] args) {

        //同時啓動1000個線程,去進行i++計算,看看實際結果

        for (int i = 0; i < 1000; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    volatileTest.inc();
                }
            }).start();
        }

        try {
            Thread.sleep(20000);
            System.out.println("運行結果:Counter.count=" + volatileTest.count);
        } catch (InterruptedException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }   
    }
}

運行結果爲:
這裏寫圖片描述

原因是因爲有多個線程訪問count,而該count可能在被修改前,轉到其他線程進行訪問,因此出現了多個993,999的值,這是併發訪問中常見的問題,所以出現了synchronized和Lock類來進行對象的同步防止數據異常。

接下來再來看volatile:

public class volatileTest {
    public volatile static int count = 0;

    public static void inc() {

        //這裏延遲1毫秒,使得結果明顯
        try {
            Thread.sleep(1);
        } catch (InterruptedException e) {
        }

        count++;
        System.out.println(Thread.currentThread().getName()+",運行結果:count=" + volatileTest.count);
    }

    public static void main(String[] args) {

        //同時啓動1000個線程,去進行i++計算,看看實際結果

        for (int i = 0; i < 1000; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    volatileTest.inc();
                }
            }).start();
        }
    }
}

結果:
這裏寫圖片描述
仍然有問題,分析原因是:
因爲count雖然爲volatile域,可以做到更新的值馬上傳遞到主存,但是由於在上述的inc()方法中,count++並不是原子性操作,可能在進行加1的過程中,即加1的過程還沒結束,其他線程開始讀取count當前數值(因此讀取到的仍是舊值),導致volatile失去了作用(個人理解,請指正)。

/* 以下內容是對volatile的理解
但是volatile還是有作用的,作用在哪?如果count加1使用原子性操作,volatile域的作用是什麼?線程0和線程1都讀取到count爲0的初值,然後線程0進行count加1(原子性操作),然後線程1進行count+1,在線程1完成該原子性操作之後,線程1中的count是1還是2?若是2是不是就是volatile和普通static變量的區別?
查閱資料,解決了:
volatile可以保證共享變量在其他線程的工作內存中變化後馬上更新到主存,然後當其他線程要讀取該共享變量時,能在主存讀取到最新的值,但是普通變量若在線程0的工作內存中變化後是不知道什麼時候纔會更新到主存的,所以又其他線程,如線程1要讀取該普通變量,還是讀到的舊值,不過對於已經讀取共享變量到工作內存的線程來說,該值是不會變的,如線程2和線程0都讀取了volatile修飾的共享變量count爲0,此時線程0完成count加1的原子性操作,主存中會馬上更新,此時線程1再來讀取count時可以讀到count爲1,而線程2中讀取的count仍爲0。

而如何保證count操作正確呢?即1000個線程對count++,不用同步或鎖,得到最後count爲1000?把count++操作改成原子性操作即可,借鑑他人代碼:

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;

public class volatileTest {
//  public volatile static int count = 0;

    public static AtomicInteger count = new AtomicInteger(0);

    public static void inc() {

//      count++;
        count.incrementAndGet();
        System.out.println(Thread.currentThread().getName()+",count="+count);
    }

    public static void main(String[] args) throws InterruptedException {      
        ExecutorService service=Executors.newFixedThreadPool(Integer.MAX_VALUE);

        for (int i = 0; i < 1000; i++) {
            service.execute(new Runnable() {
                @Override
                public void run() {
                    volatileTest.inc();
                }
            });
        }

        service.shutdown();
        //給予一個關閉時間(timeout),但是實際關閉時間應該會這個小
        service.awaitTermination(20,TimeUnit.SECONDS);       
        System.out.println("運行結果:Counter.count=" + volatileTest.count);
    }
}

結果:
這裏寫圖片描述
這個結果是正確的,思路也應當是對的。
至此其實已經解決了atomic類的特點對不採用同步且防止多線程帶來的數據異常問題了。
原子類爲什麼可以保證及時更新,不需要volatile關鍵字呢?因爲其內部其實是一個volatile的域,所以不需要在外面加該關鍵字了
AtomicInteger源碼(參考 http://wentao365.iteye.com/blog/1789750

private volatile int value;  

關於volatile的資料:
volatile具有synchronized關鍵字的“可見性”,但是沒有synchronized關鍵字的“併發正確性”,也就是說不保證線程執行的有序性。

也就是說,volatile變量對於每次使用,線程都能得到當前volatile變量的最新值。但是volatile變量並不保證併發的正確性。

看下面的例子:

假如count變量是volatile的。線程1,線程2 在進行read,load 操作中,發現主內存中count的值都是5,那麼都會加載這個最新的值在線程1堆count進行修改之後,會write到主內存中,主內存中的count變量就會變爲6,線程2由於已經進行read,load操作,在進行運算之後,也會更新主內存count的變量值爲6,導致兩個線程及時用volatile關鍵字修改之後,還是會存在併發的情況。

在 java 垃圾回收整理一文中,描述了jvm運行時刻內存的分配。其中有一個內存區域是jvm虛擬機棧,每一個線程運行時都有一個線程棧,

線程棧保存了線程運行時候變量值信息。當線程訪問某一個對象時候值的時候,首先通過對象的引用找到對應在堆內存的變量的值,然後把堆內存

變量的具體值load到線程本地內存中,建立一個變量副本,之後線程就不再和對象在堆內存變量值有任何關係,而是直接修改副本變量的值,

在修改完之後的某一個時刻(線程退出之前),自動把線程變量副本的值回寫到對象在堆中變量。這樣在堆中的對象的值就產生變化了。下面一幅圖

描述這寫交互(描述的是非原子性的操作,若是原子性操作加上volatile,如基本類型變量賦值,主存中該變量會立即更新,以供外界讀取最新值)
這裏寫圖片描述

read and load 從主存複製變量到當前工作內存
use and assign 執行代碼,改變共享變量值
store and write 用工作內存數據刷新主存相關內容

其中use and assign 可以多次出現

但是這一些操作並不是原子性,也就是 在read load之後,如果主內存count變量發生修改之後,線程工作內存中的值由於已經加載,不會產生對應的變化,所以計算出來的結果會和預期不一樣

對於volatile修飾的變量,jvm虛擬機只是保證從主內存加載到線程工作內存的值是最新的

例如假如線程1,線程2 在進行read,load 操作中,發現主內存中count的值都是5,那麼都會加載這個最新的值

在線程1堆count進行修改之後,會write到主內存中,主內存中的count變量就會變爲6

線程2由於已經進行read,load操作,在進行運算之後,也會更新主內存count的變量值爲6

導致兩個線程及時用volatile關鍵字修改之後,還是會存在併發的情況。

參考:
http://www.cnblogs.com/dolphin0520/p/3920373.html

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章