文章目錄
- Volatile原理
- 1.Volatile語義中的內存屏障
- 1.1.volatile寫操作的內存屏障
- 1.1.1.StoreStore 屏障
- 1.1.2.StoreLoad 屏障
- 1.2.volatile讀操作的內存屏障
- 1.2.1.LoadStore屏障
- 1.2.2.LoadLoad屏障
- 2.volatile不具備原子性
- 2.1.原理
Volatile原理
1.Volatile語義中的內存屏障
在Java代碼中,volatile關鍵字主要又兩層語義
- 不同線程對volatile變量的值具有內存可見性,就是一個線程修改了某個volatile變量的值,該值對其他線程立即可見。
- 禁止指令重排序
同時volatile關鍵字不僅能保證可見性,還能保證有序性,保證有序性是通過內存屏障指令來確保的。
JVM編譯器會在生成字節碼文件時,會在指令序列中插入內存屏障來禁止特定類型的CPU重排序。
JVM在處理volatile
關鍵字修飾的變量時,會采取保守策略來確保內存可見性和有序性,這涉及到內存屏障(Memory Barrier)的使用。內存屏障是一種硬件層面的指令,用于確保某些內存訪問操作的執行順序,防止CPU的亂序執行對并發程序的正確性產生影響。對于volatile
變量的讀寫,JVM會分別在讀和寫操作前后插入適當類型的內存屏障,確保以下幾點:
- 全局可見性: 保證對
volatile
變量的寫操作能立即對其他線程可見,即使在不同的CPU緩存中也是如此。這意味著寫操作之后,修改的值會立刻刷出到主內存中。 - 禁止重排序: 防止編譯器和CPU對涉及
volatile
變量的代碼進行不必要的重排序,確保它們按照程序員指定的順序執行。這對于依賴于特定順序的并發控制邏輯至關重要。
基于保守策略的volatile
操作內存屏障插入策略主要包括以下方面:
- 寫屏障(Store Memory Barrier): 在寫入
volatile
變量之后插入。它的作用是確保在該屏障之前的所有普通寫操作(非volatile
)都已完成,并且將當前線程的工作內存中的volatile
變量值刷新到主內存中。這樣,任何讀取該volatile
變量的線程都能看到最新值。- 在每個volatile
寫
操作前
面插入一個StoreStore
屏障 - 在每個volatile
寫
操作后
面插入一個StoreLoad
屏障
- 在每個volatile
- 讀屏障(Load Memory Barrier): 在讀取
volatile
變量之前插入。它的作用是確保讀取操作之后的加載不會被重排序到該屏障之前,并且使CPU讀取主內存中的最新值,而不是使用緩存中的舊值,從而確保讀取到的是最近一次寫入的值,無論這個寫入操作發生在哪個線程中。- 在每個volatile
讀
操作前
面插入一個LoadLoad
屏障 - 在每個volatile
讀
操作后
面插入一個LoadStore
屏障
- 在每個volatile
這些屏障的聯合使用確保了對volatile
變量的讀寫操作具有原子性和全局有序性,盡管它們不保證復合操作(如count++
)的原子性。這就是為什么即使在沒有鎖的情況下,volatile
也能作為輕量級的同步機制,用于狀態標記、雙重檢查鎖定模式等場景。
1.1.volatile寫操作的內存屏障
volatile寫操作的內存屏障插入策略為:在每個
volatile
寫操作前插入SotreStore(SS)
屏障,在寫操作之后加上StoreLoad
屏障
1.1.1.StoreStore 屏障
定義與作用: StoreStore
屏障主要用于確保一個存儲(寫)操作在另一個存儲操作之前完成。換句話說,它強制所有在該屏障之前的存儲操作在該屏障之后的存儲操作之前完成。這種屏障通常用于避免寫-寫沖突導致的數據不一致問題,尤其是在處理器有亂序執行能力的體系結構中。
- 前面的寫入不會重排序到后面
- 前面的寫指令完成后,高速緩存數據刷入主存
- 后面的寫操作不會排序到前面
應用場景: 例如,在實現某些類型的鎖釋放操作時,可能需要確保解鎖操作前的所有寫操作已經完成,以免新獲得鎖的線程看到不一致的狀態。
1.1.2.StoreLoad 屏障
定義與作用: StoreLoad
屏障是Java內存模型中最強大的一種屏障,它確保在屏障之前的所有寫操作(存儲操作)在屏障之后的任何讀操作(加載操作)之前完成。這意味著不僅要求寫操作完成,而且要確保這些寫操作對所有線程可見。因此,StoreLoad
屏障通常用于實現volatile
變量的寫操作后,以確保寫入的值對其他線程立即可見。
- 前面的寫不會重排序到后面
- 前面的寫指令操作完成后,高速緩存數據立即刷入主存
- 讓高速緩存的數據失效,重新從主存中加載數據,保證內核的高速緩存數據一致
- 后面的讀操作不會重排序到前面
應用場景: StoreLoad
屏障直接關聯于Java中volatile
字段的寫操作實現。當一個線程修改了一個volatile
變量的值,JVM會在寫操作之后插入一個StoreLoad
屏障,以確保該寫入的值能夠立即對其他線程可見,同時刷新處理器的緩存,避免數據的臟讀。此外,它也常用于鎖釋放操作后的內存可見性保障,確保解鎖前的內存更改對后續可能獲得鎖的線程是可見的。
總結來說,StoreStore
屏障關注于維持存儲操作之間的順序,而StoreLoad
屏障則進一步確保了寫操作的全局可見性,并在寫-讀操作間建立了一個順序關系,這對于維護多線程程序的一致性和正確性至關重要。
1.2.volatile讀操作的內存屏障
volatile讀操作的內存屏障插入策略為:在每個volatile讀操作后面插入
LoadLoad(LL)
屏障和LoadStore
屏障,禁止后面的普通讀、普通寫、和前面的volatile讀操作發生重排序
1.2.1.LoadStore屏障
定義與功能: LoadStore
屏障,也稱為讀寫屏障,其主要作用是確保屏障之后的讀操作不會被重排序到屏障之前,且屏障之后的寫操作不會被重排序到屏障之前的讀操作之前。這意味著它不僅確保了讀操作不會提前,還阻止了讀之后的寫操作與讀操作之前的任何寫操作發生亂序。這在volatile讀操作的上下文中,意味著確保了讀取到的volatile變量的值不會被之后的寫操作所覆蓋或影響,保持了讀取操作的確定性。
- 前面的讀操作不會排到后面
- 讓高速緩存中的數據失效,重新從主存中加載數據
- 后面的寫操作不會排列到前面
在volatile讀操作中的應用: 當執行volatile讀操作時,Java虛擬機(JVM)會在讀取操作之后插入一個LoadStore
屏障。這個屏障的目的是確保當前線程的任何后續寫操作不會與剛完成的volatile讀操作交錯,保證了volatile讀的值不會因為之后的寫而變得無效或不一致。同時,這也間接幫助確保了volatile讀取操作后的寫操作不會與之前的volatile讀操作或普通讀操作發生沖突,維護了操作的順序性。
1.2.2.LoadLoad屏障
定義與功能: LoadLoad
屏障,或稱為讀讀屏障,它的主要職責是防止屏障之后的讀操作被重排序到屏障之前的任何讀操作之前。這意味著它確保了在屏障之后執行的任何讀操作不會因為CPU的亂序執行優化而提前到屏障之前執行。盡管LoadLoad
屏障本身在某些JMM的描述中不常直接提及,但討論內存屏障時,其概念往往隱含在維護讀操作順序性的討論中。
- 前面的讀操作不會被排到后面
- 讓高速緩存中的數據失效,重新從主存中加載數據
- 后面讀操作不會排列到前面
在volatile讀操作中的應用: 雖然直接提及LoadLoad
屏障在volatile讀操作后插入的情況較少見,通常強調的是LoadStore
和StoreLoad
屏障的作用,但理解其概念對于全面把握內存屏障如何維護順序性是有幫助的。在volatile讀操作的上下文中,可以抽象理解為,屏障的邏輯效果確保了讀取volatile變量的值不會被之后的其他讀取操作提前,保證了讀取volatile變量的順序性。不過,實際中,volatile讀操作的關鍵在于通過StoreLoad
屏障確保了對其他線程寫入volatile變量的值立即可見,同時防止了volatile讀與普通讀寫操作的不恰當重排序。
2.volatile不具備原子性
volatile能保證數據的可見性,但是volatile并不能完全保證數據的原子性。對于volatile類型的變量進行符合操作例如(i++),仍然會存在線程不安全的問題
/*** 使用 10個線程,每個線程進行1000次 ++操作,來觀察成員變量的結果是否符合我們的預期*/
public class VolatileAddDemo {private volatile int num = 0;@Test@DisplayName("測試并發情況下 volatile原子性")public void testVolatileAdd() {CountDownLatch latch = new CountDownLatch(10);ExecutorService executorService = Executors.newFixedThreadPool(10);for (int i = 0; i < 10; i++) {executorService.submit(() -> {for (int j = 0; j < 1000; j++) {num++;}// 每次執行完畢 -1latch.countDown();});}// 等待全部線程執行完畢try {latch.await();} catch (InterruptedException e) {throw new RuntimeException(e);}System.out.println("最終的結果:" + num);System.out.println("預期的結果:10000");System.out.println("相差:" + (10000 - num));}}
2.1.原理
首先來看一下JMM對變量進行讀取和寫入的操作流程
對于非volatile修飾的普通變量來說,在讀取變量的時候,JMM要求需要 報紙read
、load
的順序即可
但是,從主存中讀取 x 、y 兩個變量的值,可能的操作是 read x
-> read y
-> load y
-> load x
,它并不要求操作是連續的
對于關鍵字 volatile修飾的內存可見變量而言,具有兩個重要的語義
- 使用volatile修飾的變量在變量值發生改變時,會
立即同步到主存
,并且讓其他線程的變量副本失效
- 禁止指令重排序:用volatile修飾的變量在硬件層面上會通過在指令前后加入內存屏障來實現,編譯器通過以下規則來進行實現的。
- 使用volatile修飾的變量 **read(讀取),load(加載),use(使用)**都是連續出現的,所以每次使用變量時都要從主存讀取最新的變量值,替換私有內存的變量副本值
- 對于同一個變量的**assign(賦值),store(存儲),write(主存)**操作都是連續出現,所以每次對變量的修改都會立即同步到主存中
?但是思考一下,單線程下**( read,load,use),(assign,store,write)**同時出現沒什么問題,但是在多線程并發執行的情況下,因為單個操作具備原子性,但是多個組合的話就不具備原子性了,還是有可能會出現臟數據。
下面通過圖來了解一下 并發時可能發生產生臟數據的場景
對于復合操作,volatile變量是無法保證其原子性的,如果想要保證復合操作的原子性,那么就需要使用鎖,并在在高并發場景下,volatile變量一定要和Java顯示鎖結合使用
這里補充介紹一下 JMM內存模型的 8個 操作
操作 | 描述 | 作用的對象 |
---|---|---|
read | 讀取 | 把一個變量的值從主內存或高速緩存讀到線程的工作內存中,準備下一步的load操作 |
load | 加載入 | 把read操作從主內存讀取的變量值放入線程的工作內存中的變量副本中,此時變量才對線程可見 |
use | 使用 | 把工作內存中變量的值傳遞給執行引擎,作為運算的輸入 |
assign | 賦值 | 把執行引擎計算出的結果賦值給工作內存中的變量 |
store | 存儲 | 把工作內存中修改后的變量值寫回到主內存中 |
write | 寫出 | 把store操作從工作內存中變量的值寫入到主內存,使得其他線程可見 |
lock | 加鎖 | 作用于主內存的變量,標記變量為線程獨占,確保同一時刻只有一個線程能執行lock和unlock之間的操作 |
unlock | 解鎖 | 釋放鎖,作用于主內存的變量,允許其他線程獲取該變量的鎖并進行操作 |