Java并发编程实战——volatile
引言
Java 語言提供了一種弱同步機制——volatile 變量。它的作用是確保將變量的更新操作通知到其他線程。
當把變量聲明為volatile后,編譯器和運行時都會注意到這個變量是共享的,因此不會將該變量上的操作與其他內存操作一起重排序。
另外,volatile變量不會被緩存在寄存器或對其他處理器不可見的地方,因此在讀取volatile變量時總是返回最新寫入的值。
一、volatile的兩個核心作用
volatile 變量是一種比 synchronized 關鍵字更輕量級的同步機制,這種弱同步機制并不會加鎖,它有兩方面的作用:
可見性指的是共享變量的多線程環境下的變化可以被其他線程所察覺到。在虛擬機中,變量一般存儲于堆內存中,線程會在自己的線程空間中拷貝一個變量副本,如果線程修改了變量副本,它需要刷回到公共空間后才,新的值才能夠被其他線程獲取到,volatile 底層使用了MESI——CPU緩存一致性協議,來實現變量的及時通步。
重排序,在現代CPU架構設計中,往往為了更有效的提升CPU性能,在不影響程序邏輯的前提下,會將一些指令進行重排序。例如單例模式中,new 創建對象時,正常流程發生三個步驟:
但由于指令重拍,第一步已經生成的地址可能會立刻分配給引用,而后執行初始化,即第二和第三步顛倒。
但如果在高并發場景下,單例模式中,如果沒有對單例對象增加volatile,這時發生了重排序,在 new 指令完成默認值設置后,其他線程就會判斷為非空對象,從而讀取一個錯誤的變量。
二、volatile的典型用法
2.1 循環條件的檢查
先看下下面代碼:
public class T {/*volatile*/ boolean running = true;// 對比一下有無volatile的情況下,整個程序運行結果的區別void m() {System.out.println("m start...");while (running) {}System.out.println("m end...");}public static void main(String[] args) {T t = new T();new Thread(t::m, "t1").start();try {TimeUnit.SECONDS.sleep(1);} catch (InterruptedException e) {e.printStackTrace();}t.running = false;} }無volatile運行結果左,有volatile運行結果右:
? ? ? ? ? ? ? ? ? ? ? ? ? ?
結果分析
在上面代碼中,變量running存在于堆內存的 t 對象中。
當線程 t1 開始運行的時候,會把running值從內存中讀到 t1 線程的工作區,在運行過程中直接使用這個copy,并不會每次都去讀取堆內存,這樣,在主線程修改running 的值之后,t1 線程忙于執行while死循環(這里有個變式,如果在while循環中加入一些執行的代碼,讓線程有時間在下一次循環之前讀取一下running的值,可能結果會有不同)而感知不到,所以不會停止運行。
使用volatile ,會強制所有線程都去堆內存中讀取 running 的值。
這就是一個volatile的典型用法,即檢查某個狀態標記以判斷是否退出循環。
2.2 volatile單例
還有一種用法就是在雙重檢查邏輯中,volatile 類型的單例,它也可以確保變量的初始化及時被其他線程感知。
參考《Java常用設計模式————單例模式》
2.3 用于操作非原子64位數值變量
另外,64位的數值變量——double、long 在多線程環境下的讀取和寫入需要添加 volatile。
當線程在沒有同步的情況下讀取變量,可能會得到一個失效值,但至少是由之前某個線程設置的值,而不是一個無效的隨機的值,這種安全保證叫做最低安全性,簡單來說就是,雖然并發導致了數據不一致,但最起碼還是個舊值,不是無意義的值。
這種最低安全性適用于絕大多數變量,但 64 位的double 和long除外。JVM允許 64 位的讀操作或寫操作分解為兩個32位的操作。以long類型為例,如果對該變量的讀和寫不在同一個線程,那么可能會讀到某個值的高32位和另一個值的低32位。因此,一定記得在多線程環境下操作 double 和 long 時,加上 volatile,或者是 synchronized。
三、無法保證的原子性
volatile與synchronized區別體現在原子性上。
public class T {volatile int count = 0;void m() {for (int i = 0; i < 10000; i++)count++;}public static void main(String[] args) {T t = new T();List<Thread> threads = new ArrayList<>();for (int i = 0; i < 10; i++) {threads.add(new Thread(t::m, "t" + i));}threads.forEach((o) -> o.start());threads.forEach((o) -> {try {o.join();} catch (Exception e) {e.printStackTrace();}});System.out.println(t.count);} }上面代碼中,成員變量count 在線程之間可見,10個線程共同完成為count 自加10000的操作,并通過join()方法將10個線程結果合到一起,我們理想的計算結果應該是count 被加了10,0000次(10個線程每個線程加10000次),但是執行結果卻是:
? ? ? ? ? ??? ? ? ? ? ? ? ? ? ??? ? ? ? ? ? ? ? ? ??
結果分析
volatile保證了數據的可見性,但是沒有保證對數據操作的原子性,也就是說,共享數據可能會因高并發被同一個值覆蓋。通俗點解釋,多個線程同時改變主內存中的某個值的時候,一個線程改變了這個值,并通知給其他線程及時更新自己線程內緩沖區的副本,但是由于線程改變volatile修飾的變量后需要寫入到公共內存中+其他線程再讀取,這個過程必然會慢于其他線程寫出的速度,導致其他線程還沒來得及更新自己副本變量就執行了寫出,導致主內存中的數據被覆蓋,因此在高并發的情況下不對某個數據的寫入加鎖,即便設置了volatile可見性,依然會出現問題。
因此,volatile比synchronized速度快很多,所以,如果程序中只需要保證可見性,那就要使用volatile;而如果要同時保證可見性?+ 原子性 ,則一定要加鎖。
四、解決不一致問題(擴展)
上一節中volatile無法保證原子性,導致最后的結果遠遠小于10,0000,除了比較常規的將count++ 加鎖之外是否有其他的比較好的解決方法呢?
/*** 解決同樣的問題更高效的方法,是使用AtomicXXX類,* AtomicXXX類中的每一個方法都是原子性的,但是不能保證多個方法連續調用是原子性的。*/ public class T { // volatile int count = 0;AtomicInteger count = new AtomicInteger(0);void m() {for (int i = 0; i < 10000; i++) // count++;count.getAndIncrement();}public static void main(String[] args) {T t = new T();List<Thread> threads = new ArrayList<>();for (int i = 0; i < 10; i++) {threads.add(new Thread(t::m, "t" + i));}threads.forEach((o) -> o.start());threads.forEach((o) -> {try {o.join();} catch (Exception e) {e.printStackTrace();}});System.out.println(t.count);} }上述代碼解決了volatile無法保證原子性的問題,使用AtomicXXX類,可以保證其方法操作是原子性的,執行結果如下:
incrementAndGet()方法,可以理解為加了synchronized的count++(保證了count++的原子性),但其實它的實現并不是通過synchronized而是使用了一種系統相當底層的實現,所以AtomicXXX類中方法的效率要比synchronized高很多。所以,對于純計算的操作,建議使用AtomicXXX類。
總結
A B線程都用到了一個變量,Java默認是A線程中保留一個copy , 這樣如果B 線程修改了該變量,則A線程未必知道。
使用volatile關鍵字,會讓所有線程都會讀到變量的修改值。
但是,使用volatile并不能保證在多個線程共同修改共享變量時所帶來的不一致問題,也就是說volatile 不能代替 synchronized。
參考:《馬士兵-高并發編程》56:35-67:00 + 68:50
總結
以上是生活随笔為你收集整理的Java并发编程实战——volatile的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: dataframe合并两个表_Panda
- 下一篇: jvm gc垃圾回收机制和参数说明amp