再有人问你volatile是什么,把这篇文章也发给他。
在上一篇文章中,我們圍繞volatile關鍵字做了很多闡述,主要介紹了volatile的用法、原理以及特性。在上一篇文章中,我提到過:volatile只能保證可見性和有序性,無法保證原子性。關于這部分內容,有讀者閱讀之后表示還是不是很理解,所以我再單獨寫一篇文章深入分析一下。
volatile與有序性
在上一篇文章中我們提到過:volatile一個強大的功能,那就是他可以禁止指令重排優化。通過禁止指令重排優化,就可以保證代碼程序會嚴格按照代碼的先后順序執行。那么volatile又是如何禁止指令重排的呢?
先給出結論:volatile是通過內存屏障來來禁止指令重排的。
內存屏障(Memory Barrier) 是一類同步屏障指令,是CPU或編譯器在對內存隨機訪問的操作中的一個同步點,使得此點之前的所有讀寫操作都執行后才可以開始執行此點之后的操作。下表描述了和volatile有關的指令重排禁止行為:
從上表我們可以看出:
當第二個操作是volatile寫時,不管第一個操作是什么,都不能重排序。這個規則確保volatile寫之前的操作不會被編譯器重排序到volatile寫之后。
當第一個操作是volatile讀時,不管第二個操作是什么,都不能重排序。這個規則確保volatile讀之后的操作不會被編譯器重排序到volatile讀之前。
當第一個操作是volatile寫,第二個操作是volatile讀時,不能重排序。
具體實現方式是在編譯期生成字節碼時,會在指令序列中增加內存屏障來保證,下面是基于保守策略的JMM內存屏障插入策略:
- 在每個volatile寫操作的前面插入一個StoreStore屏障。
- 對于這樣的語句Store1; StoreStore; Store2,在Store2及后續寫入操作執行前,保證Store1的寫入操作對其它處理器可見。
- 在每個volatile寫操作的后面插入一個StoreLoad屏障。
- 對于這樣的語句Store1; StoreLoad; Load2,在Load2及后續所有讀取操作執行前,保證Store1的寫入對所有處理器可見。
- 在每個volatile讀操作的后面插入一個LoadLoad屏障。
- 對于這樣的語句Load1; LoadLoad; Load2,在Load2及后續讀取操作要讀取的數據被訪問前,保證Load1要讀取的數據被讀取完畢。
- 在每個volatile讀操作的后面插入一個LoadStore屏障。
- 對于這樣的語句Load1; LoadStore; Store2,在Store2及后續寫入操作被刷出前,保證Load1要讀取的數據被讀取完畢。
所以,volatile通過在volatile變量的操作前后插入內存屏障的方式,來禁止指令重排,進而保證多線程情況下對共享變量的有序性。
volatile與可見性
在上一篇文章中我們提到過:Java中的volatile關鍵字提供了一個功能,那就是被其修飾的變量在被修改后可以立即同步到主內存,被其修飾的變量在每次是用之前都從主內存刷新。
其實,volatile對于可見性的實現,內存屏障也起著至關重要的作用。因為內存屏障相當于一個數據同步點,他要保證在這個同步點之后的讀寫操作必須在這個點之前的讀寫操作都執行完之后才可以執行。并且在遇到內存屏障的時候,緩存數據會和主存進行同步,或者把緩存數據寫入主存、或者從主存把數據讀取到緩存。
我們在內存模型是怎么解決緩存一致性問題的?一文中介紹過緩存緩存一致性協議,同時也提到過內存一致性模型的實現可以通過緩存一致性協議來實現。同時,留了一個問題:已經有了緩存一致性協議,為什么還需要volatile?
這個問題的答案可以從多個方面來回答:
1、并不是所有的硬件架構都提供了相同的一致性保證,Java作為一門跨平臺語言,JVM需要提供一個統一的語義。
2、操作系統中的緩存和JVM中線程的本地內存并不是一回事,通常我們可以認為:MESI可以解決緩存層面的可見性問題。使用volatile關鍵字,可以解決JVM層面的可見性問題。
3、緩存可見性問題的延伸:由于傳統的MESI協議的執行成本比較大。所以CPU通過Store Buffer和Invalidate Queue組件來解決,但是由于這兩個組件的引入,也導致緩存和主存之間的通信并不是實時的。也就是說,緩存一致性模型只能保證緩存變更可以保證其他緩存也跟著改變,但是不能保證立刻、馬上執行。
- 其實,在計算機內存模型中,也是使用內存屏障來解決緩存的可見性問題的(再次強調:緩存可見性和并發編程中的可見性可以互相類比,但是他們并不是一回事兒)。寫內存屏障(Store Memory Barrier)可以促使處理器將當前store buffer(存儲緩存)的值寫回主存。讀內存屏障(Load Memory Barrier)可以促使處理器處理invalidate queue(失效隊列)。進而避免由于Store Buffer和Invalidate Queue的非實時性帶來的問題。
所以,內存屏障也是保證可見性的重要手段,操作系統通過內存屏障保證緩存間的可見性,JVM通過給volatile變量加入內存屏障保證線程之間的可見性。
內存屏障
再來總結一下Java中的內存屏障:用于控制特定條件下的重排序和內存可見性問題。Java編譯器也會根據內存屏障的規則禁止重排序。
volatile與原子性
在以前的文章中,我們介紹synchronized的時候,提到過,為了保證原子性,需要通過字節碼指令monitorenter和monitorexit,但是volatile和這兩個指令之間是沒有任何關系的。volatile是不能保證原子性的。
網上有很多文章,拿i++的例子說明volatile不能保證原子性,然后進行各種分析,有的說由于引入內存屏障導致無法保證原子性,有的說一段i++代碼,在編譯后字節碼為:
10: getfield #2 // Field i:I14: iconst_115: iadd16: putfield #2 // Field i:I在不考慮內存屏障的情況下,一個i++指令也包含了四個步驟。
這些分析,只是說明了i++本身并不是一個原子操作,即使使用volatile修飾i,也無法保證他是一個原子操作。并不能解釋為什么volatile為啥不能保證原子性。
要我說,由于CPU按照時間片來進行線程調度的,只要是包含多個步驟的操作的執行,天然就是無法保證原子性的。因為這種線程執行,又不像數據庫一樣可以回滾。如果一個線程要執行的步驟有5步,執行完3步就失去了CPU了,失去后就可能再也不會被調度,這怎么可能保證原子性呢。
為什么synchronized可以保證原子性 ,因為被synchronized修飾的代碼片段,在進入之前加了鎖,只要他沒執行完,其他線程是無法獲得鎖執行這段代碼片段的,就可以保證他內部的代碼可以全部被執行。進而保證原子性。
但是synchronized對原子性保證也不絕對,如果真要較真的話,一旦代碼運行異常,也沒辦法回滾。所以呢,在并發編程中,原子性的定義不應該和事務中的原子性一樣。他應該定義為:一段代碼,或者一個變量的操作,在沒有執行完之前,不能被其他線程執行。
那么,為什么volatile不能保證原子性呢?因為他不是鎖,他沒做任何可以保證原子性的處理。當然就不能保證原子性了。
總結
本文在上一篇文章的基礎上,再次介紹了volatile和原子性、有序性以及可見性之間的關系。有序性和可見性是通過內存屏障實現的。而volatile是無法保證原子性的。
參考資料
深入理解Java內存模型(四)——volatile
總結
以上是生活随笔為你收集整理的再有人问你volatile是什么,把这篇文章也发给他。的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 那些和闰年相关的 Bug
- 下一篇: NYOJ 714 Card Trick