上一篇學習了synchronized的關鍵字,synchronized是阻塞式同步,在線程競爭激烈的狀況下會升級爲重量級鎖,而volatile是一個輕量級的同步機制。html
前面學習了Java的內存模型,知道各個線程會將共享變量從主內存中拷貝到工做內存,而後執行引擎會基於工做內存中的數據進行操做處理。一個CPU中的線程讀取主存數據到CPU緩存,而後對共享對象作了更改,但CPU緩存中的更改後的對象尚未flush到主存,此時線程對共享對象的更改對其它CPU中的線程是不可見的。java
而volatile修飾的變量給java虛擬機特殊的約定,線程對volatile變量的修改會馬上被其餘線程所感知,即不會出現數據髒讀的現象,從而保證數據的「可見性」。緩存
咱們能夠先簡單的理解:被volatile修飾的變量可以保證每一個線程可以獲取該變量的最新值,從而避免出現數據髒讀的現象。安全
1、三個特性
在分析volatile以前,咱們先看下多線程的三個特性:原子性,有序性和可見性。多線程
1.1 原子性
原子性是指一個操做是不可中斷的,要麼所有執行成功要麼所有執行失敗。即多個線程一塊兒執行的時候,一個操做一旦開始,就不會被其餘線程所幹擾。app
看下面幾行代碼:ide
int a = 10; //語句1 a++; //語句2 int b=a; //語句3 a = a+1; //語句4
上面的4行代碼中,只有語句1纔是原子操做。學習
語句1直接將數值10賦值給a,也就是說線程執行這個語句的會直接將數值10寫入到工做內存中。spa
語句2實際上包含了三個操做:1. 讀取變量a的值;2:對a進行加一的操做;3.將計算後的值再賦值給變量a。線程
語句3包含兩個操做:1:讀取a的值;2:再將a的值寫入工做內存。
語句4與語句2相似,也是三個操做。
從這裏能夠看出,只有簡單的讀取、賦值(並且必須是將數字賦值給某個變量,變量之間的相互賦值不是原子操做)纔是原子操做。
1.2 有序性
有序性是指程序執行的順序按照代碼的前後順序執行。
Java內存模型具有一些先天的「有序性」,即不須要經過任何手段就可以獲得保證的有序性,這個一般也稱爲 happens-before 原則。若是兩個操做的執行次序沒法從happens-before原則推導出來,那麼它們就不能保證它們的有序性,虛擬機能夠隨意地對它們進行重排序。
前面線程安全篇中學習過happens-before原則,能夠去前篇看看。
1.3 可見性
可見性是指當一個線程修改了共享變量後,其餘線程可以當即得知這個修改。
而普通的共享變量不能保證可見性,由於普通共享變量被修改以後,何時被寫入主存是不肯定的,當其餘線程去讀取時,此時內存中可能仍是原來的舊值,所以沒法保證可見性。
synchronized可以保證任一時刻只有一個線程執行該代碼塊,而且在釋放鎖以前會將對變量的修改刷新到主存當中,那麼天然就不存在原子性和可見性問題了,線程的有序性固然也能夠保證。
下面咱們來看看volatile關鍵字。
2、volatile的使用
一旦一個共享變量(類的成員變量、類的靜態成員變量)被volatile修飾以後,那麼就具有了兩層語義:
-
保證了不一樣線程對這個變量進行操做時的可見性,即一個線程修改了某個變量的值,這新值對其餘線程來講是當即可見的。
-
禁止進行指令重排序。
2.1 可見性
先看下面的代碼:
public class VolatileTest { private static boolean isOver = false; private static int a = 1; public static void main(String[] args) { Thread thread = new Thread(new Runnable() { @Override public void run() { while (!isOver) { a++; } } }); thread.start(); try { Thread.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); } isOver = true; } }
這裏的代碼會出現死循環,緣由在於雖然在主線程中改變了isOver的值,可是這個值的改變對於咱們新開線程中並不可見,在線程的本地內存未被修改,因此就會出現死循環。
若是咱們用volatile關鍵字來修飾變量,則不會出現此情形
private static volatile boolean isOver = false;
這說明volatile關鍵字實現了可見性。
2.2 有序性
再看下面代碼:
public class Singleton { private volatile static Singleton instance; private Singleton() { } public Singleton getInstance() { if (instance == null) {//步驟1 synchronized (Singleton.class) {//步驟2 if (instance == null) {//步驟3 instance = new Singleton();//步驟4 } } } return instance; } }
這個是你們很熟悉的單例模式double check,在這裏看到使用了volatile字修飾,若是不使用的話,這裏可能會出現重排序的狀況。
由於instance = new Singleton()這條語句實際上包含了三個操做:
1.分配對象的內存空間;
2.初始化對象;
3.設置instance指向剛分配的內存地址。 步驟2和步驟3可能會被重排序,流程變爲1->3->2
若是2和3進行了重排序的話,線程B進行判斷if(instance==null)時就會爲true,而實際上這個instance並無初始化成功,將會讀取到一個沒有初始化完成的對象。
用volatile修飾的話就能夠禁止2和3操做重排序,從而避免這種狀況。volatile包含禁止指令重排序的語義,其具備有序性。
2.3 原子性
看下面代碼:
public class VolatileExample { private static volatile int counter = 0; public static void main(String[] args) { for (int i = 0; i < 10; i++) { Thread thread = new Thread(new Runnable() { @Override public void run() { for (int i = 0; i < 10000; i++) counter++; } }); thread.start(); } try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(counter); } }
啓10個線程,每一個線程都自加10000次,若是不出現線程安全的問題最終的結果應該就是:10*10000 = 100000;但是運行屢次都是小於100000的結果,問題在於 volatile並不能保證原子性,counter++這並非一個原子操做,包含了三個步驟:1.讀取變量counter的值;2.對counter加一;3.將新值賦值給變量counter。若是線程A讀取counter到工做內存後,其餘線程對這個值已經作了自增操做後,那麼線程A的這個值天然而然就是一個過時的值,所以,總結果必然會是小於100000的。
若是讓volatile保證原子性,必須符合如下兩條規則:
-
運算結果並不依賴於變量的當前值,或者可以確保只有一個線程修改變量的值;
-
變量不須要與其餘的狀態變量共同參與不變約束
3、實現原理
上面看到了volatile的使用,volatile可以保證可見性和有序性,那它的實現原理是什麼呢?
在生成彙編代碼時會在volatile修飾的共享變量進行寫操做的時候會多出Lock前綴的指令,Lock前綴的指令在多核處理器下會引起了兩件事情:
-
將當前處理器緩存行的數據寫回到系統內存。
-
這個寫回內存的操做會使在其餘CPU裏緩存了該內存地址的數據無效。
爲了提升處理速度,處理器不直接和內存進行通訊,而是先將系統內存的數據讀到內部緩存(L1,L2或其餘)後再進行操做,但操做完不知道什麼時候會寫到內存。若是對聲明瞭volatile的變量進行寫操做,JVM就會向處理器發送一條Lock前綴的指令,將這個變量所在緩存行的數據寫回到系統內存。可是,就算寫回到內存,若是其餘處理器緩存的值仍是舊的,再執行計算操做就會有問題。因此,在多處理器下,爲了保證各個處理器的緩存是一致的,就會實現緩存一致性協議,每一個處理器經過嗅探在總線上傳播的數據來檢查本身緩存的值是否是過時了,當處理器發現本身緩存行對應的內存地址被修改,就會將當前處理器的緩存行設置成無效狀態,當處理器對這個數據進行修改操做的時候,會從新從系統內存中把數據讀處處理器緩存裏。volatile的實現原則:
-
Lock前綴的指令會引發處理器緩存寫回內存;
-
一個處理器的緩存回寫到內存會致使其餘處理器的緩存失效;
-
當處理器發現本地緩存失效後,就會從內存中重讀該變量數據,便可以獲取當前最新值。
3.1 內存語義
理解了volatile關鍵字的大致實現原理,那對內volatile的內存語義也相對好理解,看下面的代碼:
public class VolatileExample2 { private int a = 0; private boolean flag = false; public void writer() { a = 1; flag = true; } public void reader() { if (flag) { int i = a; } } }
假設線程A先執行writer方法,線程B隨後執行reader方法,初始時線程的本地內存中flag和a都是初始狀態,下圖是線程A執行volatile寫後的狀態圖。
若是添加了volatile變量寫後,線程中本地內存中共享變量就會置爲失效的狀態,所以線程B再須要讀取從主內存中去讀取該變量的最新值。下圖就展現了線程B讀取同一個volatile變量的內存變化示意圖。
對volatile寫和volatile讀的內存語義作個總結。
-
線程A寫一個volatile變量,實質上是線程A向接下來將要讀這個volatile變量的某個線程 發出了(其對共享變量所作修改的)消息。
-
線程B讀一個volatile變量,實質上是線程B接收了以前某個線程發出的(在寫這個volatile 變量以前對共享變量所作修改的)消息。
-
線程A寫一個volatile變量,隨後線程B讀這個volatile變量,這個過程實質上是線程A經過 主內存向線程B發送消息。
3.2 內存語義的實現
咱們知道,JMM是容許編譯器和處理器對指令序列進行重排序的,但咱們也能夠用一些特殊的方式組織指令阻止指令重排序,這個方式就是增長內存屏障。咱們先來簡答瞭解下內存屏障,JMM把內存屏障指令分爲4類:
StoreLoad Barriers是一個「全能型」的屏障,它同時具備其餘3個屏障的效果。現代的多處理器大多支持該屏障(其餘類型的屏障不必定被全部處理器支持)。執行該屏障開銷會很昂貴,由於當前處理器一般要把寫緩衝區中的數據所有刷新到內存中(Buffer Fully Flush)。
瞭解完內存屏障後,咱們再來看下volatile的重排序規則:
-
當第二個操做是volatile寫時,無論第一個操做是什麼,都不能重排序。這個規則確保volatile寫以前的操做不會被編譯器重排序到volatile寫以後。
-
當第一個操做是volatile讀時,無論第二個操做是什麼,都不能重排序。這個規則確保volatile讀以後的操做不會被編譯器重排序到volatile讀以前。
-
當第一個操做是volatile寫,第二個操做是volatile讀時,不能重排序。
要實現volatile的重排序規則,須要來增長一些內存屏障,爲了保證在任意處理器平臺均可以實現,內存屏障插入策略很是保守,主要作法以下:
-
在每一個volatile寫操做的前面插入一個StoreStore屏障。
-
在每一個volatile寫操做的後面插入一個StoreLoad屏障。
-
在每一個volatile讀操做的後面插入一個LoadLoad屏障。
-
在每一個volatile讀操做的後面插入一個LoadStore屏障。
須要注意的是:volatile寫是在前面和後面分別插入內存屏障,而volatile讀操做是在後面插入兩個內存屏障
StoreStore屏障:禁止上面的普通寫和下面的volatile寫重排序;
StoreLoad屏障:防止上面的volatile寫與下面可能有的volatile讀/寫重排序
LoadLoad屏障:禁止下面全部的普通讀操做和上面的volatile讀重排序
LoadStore屏障:禁止下面全部的普通寫操做和上面的volatile讀重排序
volatile寫插入內存屏障後生成的指令序列示意圖:
volatile讀插入內存屏障後生成的指令序列示意圖:
原文出處:https://www.cnblogs.com/yuanqinnan/p/11162682.html