在程序的執行過程當中,涉及到兩個方面:指令的執行和數據的讀寫。其中指令的執行經過處理器來完成,而數據的讀寫則要依賴於系統內存,可是處理器的執行速度要遠大於內存數據的讀寫,所以在處理器中加入了高速緩存。在程序的執行過程當中,會 先將數據拷貝處處理器的高速緩存中,待運算結束後再回寫到系統內存當中。html
在單線程的狀況下不會有什麼問題,可是若是在多線程狀況下就可能會出現異常的狀況,如下面這段代碼爲例,i
是放在堆內存的共享變量:java
i = i + 1; //i 的初始值爲0。
複製代碼
假如線程A
和線程B
都執行這段代碼,那麼就可能出現下面兩種狀況:編程
A
先執行+1
操做,而後將i
的值寫回到系統內存中;線程B
從系統內存中拷貝i
的值1
到高速緩存中,執行完+1
操做再回寫到系統內存中,最終的結果是i=2
。A
和線程B
首先都將i
的值0
拷貝到各自處理器的高速緩存當中,線程A
首先執行+1
操做,以後i
的值爲1
,而後寫回到系統內存中;可是對於線程B
而言,它並不知道這一過程,在運行該線程的處理器的高速緩存中i
的值仍然爲0
,所以在它執行+1
操做後,再將i
的值寫回到系統內存中,最終的結果是i=1
。這種不肯定性就稱爲 緩存不一致。緩存
在併發編程中,有三個關鍵的概念:可見性、原子性和有序性,只有保證了這三點才能使得程序在多線程狀況下得到預期的運行結果。安全
可見性:是指線程之間的可見性,一個線程修改的狀態對另外一個線程是可見的。也就是一個線程修改的結果,另外一個線程立刻就能看到。在1.1
所舉的例子就存在可見性的問題。多線程
在Java
中volatile
、synchronized
和final
實現可見性。併發
原子性:即一個操做或者多個操做,要麼所有執行而且執行的過程不會被任何因素打斷,要麼就都不執行。框架
再好比a++
,這個操做實際是a=a+1
,是可分割的,因此它不是一個原子操做。非原子操做都會存在線程安全問題,須要咱們使用同步技術來讓它變成一個原子操做。一個操做是原子操做,那麼咱們稱它具備原子性。編程語言
在Java
中synchronized
和在lock
、unlock
中操做或者原子操做類來保證原子性。函數
有序性:即程序執行的順序按照代碼的前後順序執行。如下面的代碼爲例:
int i = 0;
boolean flag = false;
i = 1; //語句1
flag = true; //語句2
複製代碼
在上面的代碼中定義了一個整形和Boolean
型變量,並經過語句1
和語句2
對這兩個變量賦值,可是JVM
在執行這段代碼的時候並不保證語句1
在語句2
以前執行,也就是說可能會發生 指令重排序。
指令重排序指的是在 保證程序最終執行結果和代碼順序執行的結果一致的前提 下,改變語句執行的順序來優化輸入代碼,提升程序運行效率。
可是這一前提條件在多線程的狀況下就有可能出現問題,如下面的代碼爲例:
//線程1:
context = loadContext(); //語句1
inited = true; //語句2
//線程2:
while (!inited) {
sleep()
}
doSomethingWithConfig(context);
複製代碼
對於線程1
來講,語句1
和語句2
沒有依賴關係,所以有可能會發生指令重排序的狀況。可是對於線程2
來講,語句2
在語句1
以前執行,那麼就會致使進入doSomethingWithConfig
函數的時候context
沒有初始化。
Java
語言提供了volatile
和synchronized
兩個關鍵字來保證線程之間操做的有序性,volatile
是由於其 自己包含禁止指令重排序 的語義,synchronized
是由 一個變量在同一個時刻只容許一條線程對其進行 lock 操做 這條規則得到的,此規則決定了持有同一個對象鎖的兩個同步塊只能串行執行。
volatile
的定義以下:Java
編程語言容許線程訪問共享變量,爲了確保共享變量能被準確和一致地更新,線程應該確保 經過排它鎖單獨地得到這個變量。若是一個字段被聲明成volatile
,Java
線程內存模型確保 全部線程看到這個變量的值是一致的。
一旦一個共享變量被volatile
修飾以後,那麼就具有了兩層語義:
下面,咱們用兩個小結解釋一下這兩層語義。
當咱們在X86
處理器下經過工具獲取JIT
編譯器生成的彙編指令,來查看對volatile
進行寫操做時,會發生下面的事情:
//Java 代碼
instance = new Singleton(); //instance 是 volatile 變量
//轉變成彙編代碼
0x01a3de1d: move $0 x 0, 0 x 1104800 (%esi);
0x01a3de24: lock add1 $ 0 x 0, (%esp);
複製代碼
有volatile
變量修飾的共享變量 進行寫操做的時候 會多出兩行彙編代碼,Lock
前綴的指令在多核處理器下引起了兩件事情:
volatile
關鍵字禁止指令重排序有兩層意思:
volatile
變量的讀操做或者寫操做時,在其前面的操做的更改確定所有已經進行,且結果已經對後面的操做可見;在其後面的操做確定尚未進行;volatile
變量訪問的語句放在其後面執行,也不能把volatile
變量後面的語句放到其前面執行。如下面的例子爲例:
//flag 爲 volatile 變量
x = 2; //語句1
y = 0; //語句2
flag = true; //語句3
x = 4; //語句4
y = -1; //語句5
複製代碼
因爲flag
爲volatile
變量,所以,能夠保證語句1/2
在語句3
以前執行,語句4/5
在其以後執行,可是並不保證語句1/2
之間或者語句4/5
之間的順序。
對於1.2.3
舉的有關Context
問題,咱們就能夠經過將inited
變量聲明爲volatile
,這樣就會保證loadContext()
和inited
賦值語句之間的順序不被改變,避免出現inited=true
可是Context
沒有初始化的狀況出現。
volatile
相對於synchronized
的優點主要緣由是兩點:簡易和性能。若是從讀寫兩方便來考慮:
volatile
讀操做開銷很是低,幾乎和非volatile
讀操做同樣volatile
寫操做的開銷要比非volatile
寫操做多不少,由於要保證可見性須要實現 內存界定,即使如此,volatile
的總開銷仍然要比鎖獲取低。volatile
操做不會像鎖同樣 形成阻塞。以上兩個條件代表,能夠被寫入volatile
變量的這些有效值 獨立於任何程序的狀態,包括變量的當前狀態。大多數的編程情形都會與這兩個條件的其中之一衝突,使得volatile
不能像synchronized
那樣廣泛適用於實現線程安全。
所以,在可以安全使用volatile
的狀況下,volatile
能夠提供一些優於鎖的可伸縮特性。若是讀操做的次數要遠遠超過寫操做,與鎖相比,volatile
變量一般可以減小同步的性能開銷。
要使volatile
變量提供理想的線程安全,必須同時知足如下兩個條件:
x++
這樣的增量操做,它其實是一個由讀取、修改、寫入操做序列組成的組合操做,必須以原子方式執行,而volatile
不能提供必須的原子特性。避免濫用volatile
最重要的準則就是:只有在 狀態真正獨立於程序內其它內容時 才能使用volatile
,下面,咱們總結一些volatile
的應用場景。
用volatile
來修飾一個Boolean
狀態標誌,用於指示發生了某一次的重要事件,例如完成初始化或者請求停機。
volatile boolean shutdownRequested;
...
public void shutdown() { shutdownRequested = true; }
public void doWork() {
while (!shutdownRequested) {
// do stuff
}
}
複製代碼
在解釋 一次性安全發佈 的含義以前,讓咱們先來看一下 單例寫法 當中著名的 雙重檢查鎖定問題。
//使用 volatile 修飾。
private volatile static Singleton sInstance;
public static Singleton getInstance() {
if (sInstance == null) { //(0)
synchronized (Singleton.class) { //(1)
if (sInstance == null) { //(2)
sInstance = new Singleton(); //(3)
}
}
}
return sInstance;
}
複製代碼
假如 沒有使用volatile
來修飾sInstance
變量,那麼有可能會發生下面的場景:
Thread1
進入getInstance()
方法,因爲sInstance
爲空,Thread1
進入synchronized
代碼塊。Thread1
前進到(3)
處,在構造函數執行以前使sInstance
對象成爲非空,並設置sInstance
指向的內存空間。Thread2
執行,它在入口(0)
處檢查實例是否爲空,因爲sInstance
對象不爲空,Thread2
將sInstance
引用返回,此時sInstance
對象並無初始化完成。Thread1
經過運行Singleton
對象的構造函數並將引用返回給它,來完成對該對象的初始化。經過volatile
就能夠禁止第二步和第四步的重排序,也就是使得 初始化對象在設置 sInstance 指向的內存空間以前完成。
volatile bean
模式適用於將JavaBeans
做爲「榮譽結構」使用的框架。在volatile bean
模式中,JavaBean
被用做一組具備getter
和/或setter
方法的獨立屬性的容器。
volatile bean
模式的基本原理是:不少框架爲易變數據的持有者提供了容器,可是放入這些容器中的對象必須是線程安全的。
在volatile bean
模式中,JavaBean
的全部數據成員都是volatile
類型的,而且 getter
和setter
方法必須很是普通,除了獲取或設置相應的屬性外,不能包含任何邏輯。此外,對於對象引用的數據成員,引用的對象必須是有效不可變的。
public class Person {
private volatile String firstName;
private volatile String lastName;
private volatile int age;
public String getFirstName() { return firstName; }
public String getLastName() { return lastName; }
public int getAge() { return age; }
public void setFirstName(String firstName) {
this.firstName = firstName;
}
public void setLastName(String lastName) {
this.lastName = lastName;
}
public void setAge(int age) {
this.age = age;
}
}
複製代碼
若是讀操做遠遠超過寫操做,您能夠結合使用內部鎖和volatile
變量來減小公共代碼路徑的開銷。下面的代碼中使用synchronized
確保增量操做是原子的,並使用volatile
保證當前結果的可見性。若是更新不頻繁的話,該方法可實現更好的性能,由於讀路徑的開銷僅僅涉及volatile
讀操做,這一般要優於一個無競爭的鎖獲取的開銷。
public class CheesyCounter {
private volatile int value;
public int getValue() { return value; }
public synchronized int increment() {
return value++;
}
}
複製代碼
(1) Java 併發編程:volatile 關鍵字解析
(2) Java 中 volatile 關鍵字詳解
(3) 正確使用 volatile 變量
(4) volatile 的使用