volatile型變量語義講解一 :對所有線程的可見性
一、volatile變量語義一的概念
當一個變量被定義成volatile之后,具備兩個特性:
特性一:保證此變量對所有線程的可見性。這里的“可見性”是指當一條線程修改了這個變量的值,新值對於其他線程來說是可以立即得知的。而普通變量並不能做到這一點,普通變量的值在線程傳遞時均需要通過主內存來完成。 比如:線程A修改了一個普通變量的值,然后向主內存進行回寫,另一條線程B在線程A回寫完成了之后再對主內存進行讀取操作,新變量值才會對線程B可見。
二、volatile能夠保證線程安全嗎
基於volatile變量在各個線程中是不存在一致性問題的,從物理存儲的角度看,各個線程的工作內存中volatile變量也可以存在不一致的情況,但是由於每次使用前都要進行刷新,執行引擎看不到不一致的情況,因此也可以人為不存在一致性問題,但是java里面的運算操作符並非是原子操作,這導致了volatile變量的運算在並發下一樣是不安全的。
案例代碼:
/** * 測試Volatile的特性 */ public class VolatileTest { public static volatile int race = 0; public static void increase(){ race++; } //定義線程的數量 private static final int THREADS_COUNT = 20; public static void main(String[] args) { Thread[] threads = new Thread[THREADS_COUNT]; for(int i = 0;i<THREADS_COUNT;i++){ threads[i] = new Thread(new Runnable() { @Override public void run() { for(int j = 0;j<1000;j++){ increase(); } } }); threads[i].start(); System.out.println("線程"+i+"開始執行"); } while (Thread.activeCount()>2){ System.out.println("Thread.activeCount() = "+Thread.activeCount()); Thread.yield();//有其他線程等待時,將該線程設置為就緒狀態。 } System.out.println("race:"+race); } }
這段代碼發起了20個線程,每個線程都對race變量的做了10000次的自增操作,如果是正常的並發的話,那么race的結果用該是200000,可是執行幾次,發現結果並不是200000,而都是一個小於200000的值。這是為什么呢? 因為++操作本身就不是原子的,要經過讀取計算和寫回,那么,我們通過一張圖模仿一下以上代碼:
由於變量被volatile修飾,因此這張圖中的3,4操作是連續不間斷的,5,6,7的操作也是連續不間斷的,但是經過兩個線程的讀取修改寫回操作后,i的值僅僅從1變為了2,並不是我們想象的3,
可能在這里理解上述的圖和描述有點抽象,因為有的朋友可能並不能理解數據在主存和緩存中的讀取更改的傳遞規則,在這里,補充一下變量在內存之間的相互操作知識點,大家可以先看以下這塊內容,再回過頭進行理解上述圖中的操作。
三:內存間的相互操作
·lock(鎖定):作用於主內存的變量,它把一個變量標識為一條線程獨占的狀態。
·unlock(解鎖):作用於主內存的變量,它把一個處於鎖定狀態的變量釋放出來,釋放后的變量才可以被其他線程鎖定。
·read(讀取):作用於主內存的變量,它把一個變量的值從主內存傳輸到線程的工作內存中,以便隨后的load動作使用。
·load(載入):作用於工作內存的變量,它把read操作從主內存中得到的變量值放入工作內存的變量副本中。
·use(使用):作用於工作內存的變量,它把工作內存中一個變量的值傳遞給執行引擎,每當虛擬機遇到一個需要使用變量的值的字節碼指令時將會執行這個操作。
·assign(賦值):作用於工作內存的變量,它把一個從執行引擎接收的值賦給工作內存的變量,每當虛擬機遇到一個給變量賦值的字節碼指令時執行這個操作。
·store(存儲):作用於工作內存的變量,它把工作內存中一個變量的值傳送到主內存中,以便隨后的write操作使用。
·write(寫入):作用於主內存的變量,它把store操作從工作內存中得到的變量的值放入主內存的變量中。
由volatile修飾的變量的特性保證此線程的可見性可知,當我們使用volatile修飾了一個變量后,一個線程對此變量的修改對於其他線程來講是立即可知的,也就是說.assign,.store,.write這三個操作是原子的,中間不會間斷,會馬上的同步主存,就像直接操作主存一樣,並通過緩存一致性通知其他的緩存中的副本過期。普通變量可能會在.assign,.store,.write這三個操作中插入其他的操作,導致更改后的數據不能立即同步回主存,這種情況在volatile修飾變量時是不存在的。
四:使用volatile控制並發的場景
由於volatile變量只能保證可見性,在不符合以下兩條規則的運算場景中,我們仍然要通過加鎖(使用synchronized、java.util.concurrent中的鎖或原子類)來保證原子性:
1、運算結果並不依賴變量的當前值,或者能夠確保只有單一的線程修改變量的值。
2、變量不需要與其他的狀態變量共同參與不變約束。
舉個例子:
class VolatileOne{ volatile boolean isShutDown; public void shutDown(){ isShutDown = true; } public void dowork(){ while (!isShutDown){ //業務代碼 } } }
這類場景就比較適合使用volatile控制並發,當 shutDown()方法被調用時,能保證所有線程中執行的dowork()方法都立即停下來。
使用volatile變量的第二個特性是禁止指令重排優化,我們下一篇再來分析。
補充一下,本文參考資料來源於:《深入理解Java虛擬機:JVM高級特性與最佳實踐(第3版)》,其中講解race++操作時書上用了字節碼進行解釋的,我這里通過搜索資料,通過一張圖來進行的描述,希望可以直觀的幫助大家理解。
還有,昨天寫的音樂+技術+逗比的結合寫法被朋友說寫的太亂了,很無厘頭,因此這篇中規中矩的寫了一篇,整理加自身理解,作者整理不易,覺得能幫助到您的請動動小手點個贊,如有問題請評論區提出。