在討論原子性操作時,我們經常會聽到乙個說法:任意單個volatile變數的讀寫具有原子性,但是volatile++這種操作除外。
所以問題就是:為什麼volatile++不是原子性的?
因為它實際上是三個操作組成的乙個符合操作。
首先獲取volatile變數的值
將該變數的值加1
將該volatile變數的值寫會到對應的主存位址
乙個很簡單的例子:
結合記憶體屏障這個概念對volatile的讀寫操作深入理解的話:
在第一步操作的指令後,會增加兩個記憶體屏障:
在volatile讀操作後插入loadload屏障,防止前面的volatile讀與後面的普通讀重排序
在volatile讀操作後插入loadstore屏障,防止前面的volatile讀與後面的普通寫重排序
因此第乙個指令和它後續的普通讀寫操作會被保證沒有重排序來搗亂。通常是去記憶體中去讀。
那麼問題又來了,為什麼通常去記憶體中讀?
其實這個問題要說細的話可以很細,大概就兩個關鍵點吧:
volatile的寫操作的快取失效機制
最後乙個對volatile變數執行寫操作的cpu,由於在它對應的快取中保有最新的值,因此可以不用再去主存裡面獲取
具體看下面第三步的分析。
這個步驟沒什麼特別的,就是在cpu自身的快取記憶體(暫存器,l1-l3 cache)中完成。不涉及到快取和記憶體的互動。
volatile寫算是乙個重點。
根據jmm對於volatile變數型別的語義規範:volatile在編譯之後,會在變數寫操作時新增lock字首指令。這個lock字首指令在多核處理器的環境中,有這樣的作用:
通知cpu將當預處理器快取行的資料寫回到系統主存中
該寫回操作將使其他cpu快取了該記憶體位址的資料無效
另外,記憶體屏障在volatile的寫操作中起到了很大的作用,來保證上面兩點能夠實現:
在volatile寫操作前插入storestore屏障,防止前面其他寫與本次volatile寫重排序
在volatile寫操作後插入storeload屏障,防止本次的volatile寫與後面的讀操作重排序
那麼為了解決volatile++這類復合操作的原子性,有什麼方案呢?其實方案也比較多的,這裡提供兩種典型的:
使用synchronized關鍵字
使用atomicinteger/atomiclong原子型別
synchronized是比較原始的同步手段。它本質上是乙個獨佔的,可重入的鎖。當乙個執行緒嘗試獲取它的時候,可能會被阻塞住,所以高併發的場景下效能存在一些問題。
在某些場景下,使用synchronized關鍵字和volatile是等價的:
寫入變數值時候不依賴變數的當前值,或者能夠保證只有乙個執行緒修改變數值。
寫入的變數值不依賴其他變數的參與。
讀取變數值時候不能因為其他原因進行加鎖。
加鎖可以同時保證可見性和原子性,而volatile只保證變數值的可見性。
這類原子型別比鎖更加輕巧,比如atomicinteger/atomiclong分別就代表了整型變數和長整型變數。
在它們的實現中,實際上分別使用的volatile int/volatile long儲存了真正的值。因此,也是通過volatile來保證對於單個變數的讀寫原子性的。
在此基礎之上,它們提供了原子性的自增自減操作。比如incrementandget方法,這類方法相對於synchronized的好處是:它們不會導致執行緒的掛起和重新排程,因為在其內部使用的是cas非阻塞演算法。
為什麼volatile不能保證原子性
原子操作簡單來說,原子操作 atomic 就是不可分割的操作,在計算機中,就是指不會因為新城排程被打斷的操作。比如,簡單的賦值就是乙個原子操作 m 6 這是個原子操作假如m原先的值為0,那麼對於這個操作,要麼執行成功m程式設計了6,要麼沒執行m還是0,而不會出現諸如m 3,這種中間狀態。但是宣告並賦...
volatile為什麼不能保證原子性?
我們來看一下jmm是如何解釋的jmm規定了所有的變數都儲存在主記憶體 main memory 中,多個執行緒共享主記憶體中 的資料。每個執行緒都有自己的工作記憶體 working memory 執行緒的工作記憶體中保 存了該執行緒使用到的變數在主記憶體的副本拷貝,執行緒對變數的所有操作 讀取 賦 值...
volatile為什麼不能保證變數原子性
看個例子,僅是個人理解。public class volatiledemo 當進行自增操作,位元組碼是這樣的 1 獲取變數的值併入棧 2 把1入棧 3 依次取出棧中兩個運算元相加併入棧 假設為臨時變數t 4 取出棧頂值賦值給變數 而volatile的可見性指的是第4步,當修改變數後,把新值重新整理到...