Volatile併發理解

Volatile作用

在java中主要用來修飾成員變量和類變量。其中,使用volatile修飾的變量在多線程環境中對所有多線程都是可見的。即,其中一個線程修改了volatile修飾的變量值,則其他線程能夠立即得到最新修改的值。對於這個方面的理解,可以從併發編程模型(CPU,緩存,內存)和JAVA內存模型兩個方面分析。

併發編程模型

現在的服務器都是多核的,而應用的都是發生在多線程環境中,那麼在多核服務器中,多個處理器之間是如何與同一塊內存中的數據進行交互呢?這就要先了解一下硬件層面的結構模型:
這裏寫圖片描述
通過上述圖中,可以發現在一個多核的服務器中,每一個處理器都有一塊單獨屬於自己的緩存空間。每個線程將內存中的數據讀取到高速緩存中進行運算操作,等待運算完再降變量值重寫入內存中。
當多個cpu併發處理同一個變量時,是如何做到緩存之間變量值的同步呢?這就要求多個處理器之間遵守緩存協議(引用他人文章解釋):
最出名的就是Intel 的MESI協議,MESI協議保證了每個緩存中使用的共享變量的副本是一致的。它核心的思想是:當CPU寫數據時,如果發現操作的變量是共享變量,即在其他CPU中也存在該變量的副本,會發出信號通知其他CPU將該變量的緩存行置爲無效狀態,因此當其他CPU需要讀取這個變量時,發現自己緩存中緩存該變量的緩存行是無效的,那麼它就會從內存重新讀取。
這裏寫圖片描述

java內存模型

java內存模型跟上面OS併發編程模型相似,也是分爲主內存,工作內存,線程。其中,每個線程都有自己的工作內存。對於共享變量i,每個線程操作時都會將其拷貝一份到工作內存,每個線程操作的都是工作內存中的變量,禁止直接操作主內存中的數據。內存模型如下:
這裏寫圖片描述
根據內存模型,可以看出對於主內存中的共享變量,每個線程都會有一個關於該共享變量的副本拷貝在其工作內存區。線程修改共享變量的值只能發生在工作內存區,等修改完畢後,會將最新值rewrite到主內存中。
對於單線程操作肯定是沒有問題的,但是對多線程的操作,程序的執行結果就很容易發生錯誤。當然,對於多線程,在java中可以有很多種方式避免,比如同步(synchronized,lock,volatile)都可以做到同步。關於java內存模型可以看看我另外一篇分享的文章

volatile深探

看完上訴併發編程模型和java內存模型簡單分析之後,應該大致對併發編程處理機制有個初步的瞭解。volatile在修飾變量(成員變量,類變量)到底起到什麼作用,可以通過三個方面進行分析:

原子性

原子操作對於併發編程來說無疑是最重要的操作。原子操作指示要麼全部成功要麼全部失敗。volatile修飾的變量對於其他線程是可見的,但是變量的操作不是原子操作。只有基本類型操作(賦值,讀取)纔是原子性操作。其他類型變量或基本類型的運算操作都是非原子操作。通過代碼演示volatile修飾變量的非原子性操作:
public class SingletonInstance {

    private volatile static SingletonInstance instance = null;

    private SingletonInstance(){}

    public static SingletonInstance getInstance (){
        if(null == instance){    
            instance = new  SingletonInstance();
        }
        return instance;
    }
}

上述代碼是想實現一個單例模式,即多線程環境下生成的SingletonInstance實例是唯一的。看看調用代碼:

public class SingletonInstanceTest {

    public static void main(String[] args) {
        for(int i=0;i<30;i++){
             new Thread(new ThreadDemo()).start();
        }
    }
}

class ThreadDemo implements Runnable{
    public void run() {
        System.out.println(SingletonInstance.getInstance());
    }

}

上述多線程環境下運行程序結果如下:
這裏寫圖片描述
可以看到在上述環境下使用volatile修飾的變量並不能做到線程安全。就是因爲裏面涉及了非原子的操作。下面通過一張圖來理解volatile的執行原理
這裏寫圖片描述
1. 根據圖中流程,線程A將主內存變量拷貝到工作內存經過運算之後再同步到主內存主要經過了8個步驟。
2. read:線程從主內存將共享變量var值(初始化一定在主內存完成)讀取到工作內存,即結束。
3. load:線程執行完read操作之後就跟主內存變量沒有關係。load是將主內存變量值賦給工作內存中的變量副本
4. use:使用load操作完工作內存中的變量副本,進行一定的運算。
5. assign:將use動作運算的結果賦值給變量副本var_v。
6. store:將新的運算結果的變量值存儲到主內存。
7. write:作用於主內存變量,把store存儲的值重新賦給主內存變量。
通過上述分析,可以發現volatile修飾的變量只是解決了變量在讀取時對於所有 線程都是可見的。但是後續對於變量的操作不是原子性的。多線程對於volatile修飾 變量操作問題,還是需要通過加鎖進行同步。這也就解釋了上述程序中,爲什麼多線程環境中獲得SingletonInstance的實例不是唯一的。
工作線程每次操作volatile修飾的變量,都需要先從主內存刷新最新的變量值,用於保證其他線程對該變量修改的可見性

可見性

  1. volatile修飾變量對於多線程讀取時是可見的。也就是說如果線程A修改了共享變量var的值,線程B,C,D再次讀取時,一定能夠讀取到最新的var值。因爲使用volatile修飾的變量值,如果發生了改變,就會立即同步到內存。同理,其他線程每次讀取該變量時都要重新從主內存讀取。
    這裏寫圖片描述

有序性

JVM中對於某一些規定有着天然有序性.這種有序性實在某些條件下才會成立的。在使用volatile修飾後,JVM遵從如下有序性:
1. 程序代碼中volatile修飾變量V前的代碼一定優先發生在V之前執行,不會在V之後執行。
2. 程序代碼中V之後定義的代碼一定在V之後執行,絕對不會在V之前執行。

總結

  1. volatile是JAVA中最輕量級的同步機制,只能是修飾變量,不能修飾方法和代碼塊
  2. volatile修飾的變量對於所有線程讀取時是可見的,即一個線程對共享變量值的修改對於其他線程再次讀取時是可見的。
  3. volatile修飾的變量在多線程環境中的操作是非原子性的,需要使用加鎖進行同步。

參考資料

JAVA多線程編程核心藝術
深入理解JVM虛擬機
http://www.cnblogs.com/dolphin0520/p/3920373.html

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