java 輕量級同步volatile關鍵字簡介與可見性有序性與synchronized區別

 

目錄

 概念

volatile與synchronized對比

總結


概念

JMM規範解決了線程安全的問題,主要三個方面:原子性、可見性、有序性,藉助於synchronized關鍵字體現,可以有效地保障線程安全(前提是你正確運用)

之前說過,這三個特性並不一定需要全部同時達到,在有些場景,部分達成也能夠做到線程安全。

volatile就是這樣一個存在,對可見性和有序性進行保障

image_5c6df9b1_201d

可見性

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保障了可見性與有序性,可以推斷出來並沒有做到原子性

image_5c6df9b1_5ac2

問題出在哪裏?

關鍵在於count++;自增操作,並不是直接的賦值操作,比如x=1;

他可以簡單的理解爲三個步驟:

  1. 讀取count的值;
  2. 操作count的值;
  3. 回寫count的值;

volatile可以保障在第一步的時候,讀取到了正確的值,但是由於不是原子的,在接下來的操作過程中,count的值,可能已經被更新過了,也就是讀取到了舊值

繼續使用這個舊值很顯然就把別人的更新抹掉了,你讀取的1,可能此時應該是2了,但是你操作後還是2,無故的擦除了別人的增加,所以結果纔會出現小於10000的情況

因爲是自增操作,所以使用舊值會導致小於10000

如果把初始值設置爲10000,使用自減count--,使用舊值就可能會導致別人的減量被擦除了,最終大於0,不妨修改爲自減運算試一下

從結果看得出來,我們的推斷沒錯,就是使用了舊值

image_5c6df9b1_10ab

這就是前面說到的線程安全,單純的依賴可見性是不能保障的,還需要依賴原子性

因爲在第一步的時候,儘管獲取到的值肯定是最新的,但是接下來的過程中呢?

值仍舊可能被改變,因爲並不是原子的

比如,裝着飲料的瓶子,你從其中取飲料

可見性可以保障你要倒飲料的時候,瓶子裏面是可樂你到出來的是可樂,裝的是雪碧,倒出來就是雪碧,但是如果你把可樂倒進自己的杯子裏面了,瓶子瞬間換成雪碧,你杯子裏面的可樂會變化嗎?

 

回想下之前設計模式中介紹過的單例模式,有一種實現方式是雙重檢查法

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;

 

使用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對比

image_5c6df9b1_1ad5

總結

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關鍵字進行同步,進一步對原子性進行保障。

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