概念
JMM規範解決了線程安全的問題,主要三個方面:原子性、可見性、有序性,藉助於synchronized關鍵字體現,能夠有效地保障線程安全(前提是你正確運用)
以前說過,這三個特性並不必定須要所有同時達到,在有些場景,部分達成也可以作到線程安全。
volatile就是這樣一個存在,對可見性和有序性進行保障
可見性
volatile字面意思,易變的,不穩定的,在Java中含義也是如此
想要保證可見性,就要保障一個線程對於數據的操做,可以及時的對其餘線程可見
volatile會通知底層,指示這個變量讀取時,不要經過本地緩存,而是直接去主存中讀取(或者說本地內存失效,必須去主存讀取),這樣若是一個線程對於數據完成寫入到主存,另外線程進行讀取時,就能夠第一時間讀取到新值,而非舊值,因此所謂不穩定,就是指可能會被其餘線程同時併發修改,因此你要去主存中去從新讀取。
他會讓寫線程沖刷寫緩存,讀線程刷新讀緩存,簡言之就是操做後馬上會刷新數據,讀取前也會刷新數據;
以保證最新值能夠及時更新到主存以及讀線程及時的讀取到最新值。
注意:
若是Reader對於這個共享變量x的讀取操做有不少個步驟,好比x=1;y=x;y=y+1;y=y+2;等等 最後x=y;,若是沒有原子性保障,很顯然,若是已經執行過了y=x;再日後的操做過程當中,若是x的值再次被改變了,此時Reader中的y是沒法改變的,這就出現問題了
因此此處的可見性要注意區分,在某些場景想要線程安全的話,可見性對原子性是有依賴的
可見性指的是在你須要的時刻,若是被別人修改了,從新讀取新的,可是若是你用過了,單純的可見性並不能保證後續沒問題。
有序性
volatile關鍵字將會直接禁止JVM和處理器對關鍵字修飾的指令重排序,可是對於volatile關鍵字修飾的先後的、無依賴的指令,能夠進行重排序
被volatile修飾的變量,能夠認爲插入了一個內存屏障,他會進行以下保障:
- 確保指令重排序時不會將其後面的代碼排到內存屏障以前
- 確保指令重排序時不會將其前面的代碼排到內存屏障以後
- 確保在執行到內存屏障修飾的指令時前面的代碼所有執行完成
- 強制將線程工做內存中值的修改刷新至主內存中
- 若是是寫操做,則會致使其餘線程工做內存(CPU Cache)中的緩存數據失效
好比
int x = 0;
int y = 1;
volatile int z=20;
x++;
y--;
在語句volatile int z=20以前,先執行x的定義仍是先執行y的定義,咱們並不關心,只要可以百分之百地保證在執行到z=20的時候x=0, y=1,同理關於x的自增以及y的自減操做都必須在z=20之後才能發生。這個結果就是上面的邏輯處理後的結果。
綜上所述,volatile能夠對可見性以及有序性進行保障。
那麼volatile的原子性如何?
原子性
以下面示例,共享變量count是volatile的,在add方法中,對他進行自增,運行幾回後分別查看結果
package test1;
public class T12 {
public static volatile int count = 0;
public static void add() {
count++;
}
public static void main(String[] args) {
//建立10個線程,每一個線程循環1000次,最終結果應該是10,000
for (int i = 0; i < 10; i++) {
new Thread(() -> {
for (int j = 0; j < 1000; j++) {
add();
}
}).start();
}
// 確認其餘線程都結束了,不然不繼續執行(確認當前線程組以及子線程組活動線程的個數,JDK8中這個值設置爲2),後續有更好的方法完成等待
while (Thread.activeCount() > 2) {
Thread.yield();
}
System.out.println("count: " + count);
}
}
10個線程,每一個線程1000次循環,按理來講最終的結果應該是1000
從結果能夠看得出來,並非線程安全的,可是既然volatile保障了可見性與有序性,能夠推斷出來並無作到原子性
問題出在哪裏?
關鍵在於count++;自增操做,並非直接的賦值操做,好比x=1;
他能夠簡單的理解爲三個步驟:
- 讀取count的值;
- 操做count的值;
- 回寫count的值;
volatile能夠保障在第一步的時候,讀取到了正確的值,可是因爲不是原子的,在接下來的操做過程當中,count的值,可能已經被更新過了,也就是讀取到了舊值
繼續使用這個舊值很顯然就把別人的更新抹掉了,你讀取的1,可能此時應該是2了,可是你操做後仍是2,無端的擦除了別人的增長,因此結果纔會出現小於10000的狀況
由於是自增操做,因此使用舊值會致使小於10000
若是把初始值設置爲10000,使用自減count--,使用舊值就可能會致使別人的減量被擦除了,最終大於0,不妨修改成自減運算試一下
從結果看得出來,咱們的推斷沒錯,就是使用了舊值
這就是前面說到的線程安全,單純的依賴可見性是不能保障的,還須要依賴原子性
由於在第一步的時候,儘管獲取到的值確定是最新的,可是接下來的過程當中呢?
值仍舊可能被改變,由於並非原子的
好比,裝着飲料的瓶子,你從其中取飲料
可見性能夠保障你要倒飲料的時候,瓶子裏面是可樂你到出來的是可樂,裝的是雪碧,倒出來就是雪碧,可是若是你把可樂倒進本身的杯子裏面了,瓶子瞬間換成雪碧,你杯子裏面的可樂會變化嗎?
回想下以前設計模式中介紹過的單例模式,有一種實現方式是雙重檢查法
public class LazySingleton {
private LazySingleton() {
}
private static volatile LazySingleton singleton = null;
public static LazySingleton getInstance() {
if (singleton == null) {
synchronized (LazySingleton.class) {
if (singleton == null) {
singleton = new LazySingleton();
}
}
}
return singleton;
}
}
注意:
private static volatile LazySingleton singleton = null;html
使用volatile修飾
由於實例建立語句:singleton = new LazySingleton(); ,就不是一個原子操做
他可能須要下面三個步驟
- 分配對象須要的內存空間
- 將singleton指向分配的內存空間
- 調用構造函數來初始化對象
計算機爲了提升執行效率,會作的一些優化,在不影響最終結果的狀況下,可能會對一些語句的執行順序進行調整
也就是上面三個步驟的順序是不可以保證惟一的
若是先分配對象須要的內存,而後將singleton指向分配的內存空間,最後調用構造方法初始化的話
假如當singleton指向分配的內存空間後,此時被另外線程搶佔(因爲不是原子操做因此可能被中間搶佔)
線程2此時執行到第一個if (singleton == null)
此時不爲空,那麼不須要等待線程1結束,直接返回singleton了
顯然,此時的singleton都尚未徹底初始化,就被拿出去使用了
根本問題就在於寫操做未結束,就進行了讀操做
重排序致使了線程的安全問題
此時能夠給 singleton 的聲明加上volatile關鍵字,以保障有序性
上面的兩個示例,看起來都是沒有保障原子性,可是爲何一個使用volatile修飾就能夠,而另一個則不行?
對於count++,運算結果的正確性依賴count當前的值自己,並且可能存在多個線程對他進行修改,而singleton則不依賴,並且也不會多個線程進行修改
因此說,volatile的使用要看具體的場景,這也是爲何被稱之爲輕量級的synchronized的緣由,他不能從原子性、可見性、有序性三個角度進行保障。
因此從上面這些點也能夠看得出來,volatile並不能替代synchronized,很關鍵的一個點就是他並不能保障原子性
volatile與synchronized對比
總結
volatile是一種輕量級的同步方式(輕量級的synchronized,也就是閹割版的synchronized)
拋開性能的角度看,synchronized的正確使用能夠百分百解決同步問題,可是volatile卻並不能徹底解決同步問題,由於他缺少一個很重要的保障---原子性
原子性可以保障不可分割,一旦不能對原子性進行保障,一旦一個變量的修改依賴自身,好比i++,也就是i=i+1;依賴自身的值,一旦再多線程環境中,仍舊可能會出錯
因此若是換一個思路理解的話,能夠這樣:
對於線程安全問題,主要是三個方面,原子性、可見性、有序性,不過並不必定全部的場景都須要三者徹底保障;
對於synchronized關鍵字都進行了保障,能夠用於線程安全的同步問題
對於volatile,他對可見性和有序性進行了保障,因此若是在有些場景下,若是僅僅保障了這二者就能夠達到線程安全,那麼volatile也能夠用於線程的同步
因此說synchronized能夠用於同步,volatile能夠用於部分場景的線程同步
剛纔提到對於i++,僅僅藉助於volatile,他至關於i=i+1,依賴自身的值的內容,因此多線程會出問題,若是隻有一個線程纔會執行這個操做就不會出現問題
另外,若是對於一個操做,好比i=j+1;j也是一個共享變量,很顯然多線程場景下,仍舊可能出現問題
因此若是你使用volatile保障線程安全,須要很是慎重,必要的時候,仍舊須要藉助於synchronized關鍵字進行同步,進一步對原子性進行保障。