本文對volatile的概念、原子性、指令重排、內存屏障、使用與場景等知識作說明,試圖爲讀者理解volatile提供幫助。
編程
一. 概念數組
volatile字面意思是易變的、不穩定的。
在Java中關鍵字volatile是一個類型修飾符,使用方式如:緩存
static volatile int i=0;
其做用是告訴虛擬機該變量是極有可能多變的,此處免於一些優化措施,不能隨意變更目標指令,並保障該變量上操做的原子性。
volatile修飾的變量有「可見性」,其含義是變量被修改後,應用程序範圍內的全部線程都可以知道這個改動。
volatile是非排他的,經常用於多線程之間的共享變量。volatile並不是鎖的替代品,但在必定條件下它比鎖更合適,性能開銷比鎖更少。
多線程
二. 原子性併發
原子性是說一個操做是不可中斷的,不可分割的。
並不是簡單的操做就是原子性的,或者複雜的操做就是非原子性的。
i++是非原子性的,該操做其實是一個read-modify-write操做,在執行過程當中其餘線程可能已經修改了i的值,所以該操做不具有不可分割性,也就不是原子操做。但若i是一個局部變量,保證了read-modify-write過程不被其餘線程干擾,那麼該操做就是一個原子的操做。
通常而言,對volatile變量的賦值操做,只要表達式右邊涉及非局部變量,該操做就不是一個原子操做。
app
又如這樣一個賦值操做:高併發
volatile HashMap map=new HashMap();
能夠分解爲以下僞代碼:性能
obj=allocate(HashMap.Class);//子操做1,分配內存
Constructor(obj);//子操做2,初始化
map=obj;//子操做3,將對象引用寫入map
雖然volatile只保障了子操做3是一個原子操做,可是因爲子操做2和子操做3僅涉及局部變量二未涉及共享變量,所以對map變量的賦值操做仍然是一個原子操做。
優化
又如對一個long型變量賦值:ui
pulic class Test { private static long lo=0; public Change(long v){ this.lo=v; } }
因爲long型長64位,在32位系統中,對long型變量賦值須要2次才能完成,期間可能有別的線程干擾,所以語義上即便只是對基本類型的一步賦值,該操做也是非原子性的。
綜上,得出認識:volatile僅保障被修飾變量自己的讀、寫操做的原子性。如要保障volatile變量賦值語句的原子性,那麼該語句中的操做不能涉及任何對共享變量—包括volatile變量自己--的訪問。
三. 指令重排
happens-before原則中有一條程序順序規則:一個線程中的每一個操做,happens-before於該線程中的任意後續操做。
通俗的講,代碼的執行是要保證從先日後,依次執行的—這是很是天然的事情。
這裏的順序實際上是從代碼的語義方面講的,在程序實際執行時,出於效率等目的在指令這一層面會對指令的前後順序進行重排。在單線程狀況下,指令重排不會影響語義邏輯,但多線程時,指令重排沒有義務也沒法確保多線程間的語義也一致。
一條指令的執行有不少細節,但簡單地說,能夠分爲幾步:
取指 IF
譯碼和取寄存器操做數 ID
執行或者有效地址計算 EX
存儲器訪問 MEM
寫回WB
完成指令須要分配CPU時間片,也涉及不一樣的硬件,如寄存器、算術邏輯單元ALU等。所以在執行各類指令時,使用的是一種流水線技術。
流水線能使CPU高效執行,滿載時效能客觀。但一旦被中斷就使全部硬件都進入一個停頓期,再次滿載須要幾個週期。爲避免效能損失,須要想辦法儘可能不讓流水線中斷。而指令重排的目的就是爲了供給連續緊湊的指令,儘可能少的中斷流水線。
下面以A=B+C這個操做爲例:
上圖中左邊的是指令,LW表示load,LW R1,B 表示把B加載到寄存器R1中。ADD指令是加法,第三條指令是R1,R2中的值相加後放入寄存器R3。SW表示store,該處命令是將R3的值保存到變量A。
右邊是流水線的狀況,注意到有兩個紅叉,表示流水線在這裏停頓,緣由是數據沒有準備好,如第三條ADD在等待寄存器R2的值。因爲ADD的停頓,後面全部的指令都要慢一拍。
看一個更復雜的例子:
a=b-c
x=y+z
上面的代碼執行以下:
因爲SUB減和ADD加指令,這裏有很多停頓。而在這裏先進行LW Ry,y和LW Rz,z這兩條加載指令對程序邏輯是沒有影響的,既然有停頓,不如利用停頓時間完成這兩步操做:
指令重排後,全部的停頓消除,流水線順暢:
四. 內存屏障
volatile能保障有序性和可見性,由於寫線程對volatile變量作寫入操做時會產生一個相似釋放鎖的效果,讀線程對volatile變量作讀取操做時會產生一個相似獲取鎖的效果。
寫線程對volatile變量作寫入操做時,虛擬機在該操做前插入一個內存釋放屏障Release Barrier,在操做完成後插入一個內存存儲屏障Store Barrier。
釋放屏障禁止寫操做及其以前的操做有指令重排,這保證了volatile寫操做前的任何讀、寫操做都先序提交,也就是說當其餘線程看到寫操做對volatile變量的更新時,以前其餘執行操做的結果對此時的讀線程都是可見的。
volatile變量讀操做時,Java虛擬機會在操做前插入一個裝載屏障Load Barrier,在操做後插入一個獲取屏障Acquire Barrier。
裝載屏障經過沖刷處理器緩存,使當前執行線程所在的處理器將其餘處理器對共享變量做的更新同步到該處理器的高速緩存中。獲取屏障禁止指令重排。
寫操做的釋放屏障與讀操做的裝載屏障一塊兒使用保障了volatile的可見性。這相似於鎖對可見性的保障,但volatile是非排他的即非阻塞的,於是volatile讀取並不能保證是時時最新的值,可能在讀取的同時有寫操做在更新共享變量。
爲有助於理解,能夠把釋放屏障(Release Barrier)設想成是打開的屏障,正在接受以前的操做完成與提交,或是根據字面意思,在release前固然要求全部操做完成;加載屏障Load Barrier是從其餘線程所在的處理器裏獲取最新,並加載到當前的處理器緩存中。
五. 數組與對象
若是volatile修飾數組,那麼volatile只能對數組引用自己起做用,沒法對數組中的元素起做用。
volatile int[] oneArrary //假設修飾一個數組
int i=oneArrary[0]; //操做1
ineArrary[1]=1;//操做2
volatile int[] twoArrary=oneArrary;//操做3
操做1中,實際上可分解爲2個子操做,子操做(1)讀取數組引用地址,因爲volatile修飾數組,這步子操做是volatile有效的,子操做(2)在取到數組引用後根據下標讀取具體元素,這步與volatile是無關的。所以操做1沒法保障volatile。
操做2中,元素與volatile無關,volatile不起做用。
操做3中,操做的都是數組引用,volatile是起做用的。
相似地,對於引用型的對象,volatile只是能保障對象的引用,至於該引用所指向的對象實例中的值volatile沒法保障。
若是要使數組內的元素也能觸發volatile做用,可使用AtomicIntegerArray、AtomicLongArray、AtomicReferenceArray。
六. 典型場景
須要注意一點,未用volatile修飾共享變量時,當虛擬機在Client模式下,JIT不會作足夠的優化,有時共享變量的更新反而對線程可見,當虛擬機在Server模式下,JIT的進行足夠的優化,有些數據副本及緩存的措施,共享變量對線程不具可見性。
public class temp { private static boolean ok;//注意這裏
private static int num; private static class WorkerThread extends Thread{ public void run(){ while (!ok){ System.out.println(num); } } } public static void main(String[] args) throws InterruptedException{ new WorkerThread().start(); Thread.sleep(1000); num=42; ok=true; Thread.sleep(10000); } }
上述代碼在Client模式下WorkerThread能夠發現判斷變更,退出程序。但在Server模式下時,優化後沒法發現變量的改動,致使程序沒法退出。
此時,須要把ok變量修飾爲volatile便可。這個場景也是volatile使用的典型場景--基於對共享變量的原子操做。
參考:[1] 葛一鳴 郭超 Java高併發程序設計 [2] 黃文海 Java多線程編程實戰指南