内存位置访问无效_万字长文——java内存模型之volatile深入解读
在閱讀本文前,請思考以下的面試題?
- volatile是什么?
- volatile的特性
- volatile是如何保證可見性的?
- volatile是如何保證有序性的?
- volatile可以保證原子性嗎?
- 使用volatile變量的條件是什么?
- volatile和synchronized的區別
- volatile和atomic原子類的區別是什么?
這一章主要是講解volatile的原理,在開始本文前,我們來看一張volatile的思維導圖,先有個直觀的認識。
什么是volatile
目前的操作系統大多數都是多CPU,當多線程對一個共享變量進行操作時,會出現數據一致性問題
Java編程語言允許線程訪問共享變量,那么為了確保共享變量能被準確和一致的更新,線程應該確保通過排他鎖單獨獲得這個變量,或者把這個變量聲明成volatile,可以理解volatile是輕量級的synchronized。
使用volatile可以在Java線程內存模型確保所有線程看到這個變量的值是一致的,在多個處理器中保證了共享變量的“可見性”。
volatile兩核心三性質
兩大核心:JMM內存模型(主內存和工作內存)以及happens-before
三條性質:原子性,可見性,有序性
volatile性質
總結:volatile保證了可見性和有序性,同時可以保證單次讀/寫的原子性
相關的Cpu術語說明
什么是可見性?
在單核cpu的石器時代,我們所有的線程都是在一顆CPU上執行,CPU緩存與內存的數據一致性容易解決。因為所有線程都是操作同一個CPU的緩存,一個線程對緩存的寫,對另外一個線程來說一定是可見的。
例如在下面的圖中,線程A和線程B都是操作同一個CPU里面的緩存,所以線程A更新了變量a的值,那么線程B之后再訪問變量 a,得到的一定是 a 的最新值(線程 A 寫過的值)。
在多核CPU的時代,每顆 CPU 都有自己的緩存,這時 CPU 緩存與內存的數據一致性就沒那么容易解決了,當多個線程在不同的CPU上執行時,這些線程操作的是不同的CPU緩存。比如下圖中,線程A操作的是CPU-1上的緩存,而線程B操作的是CPU-2上的緩存,很明顯,這個時候線程A對變量a的操作對于線程B而言就不具備可見性了。這個就屬于硬件程序員給軟件程序員挖的“坑”。
為了提高處理速度,處理器不直接和內存進行通信,而是先將系統內存的值讀到內部緩存(L1,L2或者其他)后再進行操作,但是操作完不知道何時再寫回內存。
從上面的分析,我們可以知道,多核的CPU緩存會導致的可見性問題。
volatile是如何保證可見性的
instance = new Singleton();//instance是volatile變量讓我們來看看在處理器下通過工具獲取JIT編譯器生成的匯編指令來查看對volatile進行寫操作的時候,cpu會做什么事?
轉換成匯編代碼如下:
file
有volatile修飾的共享變量進行寫操作的時候會多出第二行匯編代碼,也就是jvm會向處理器發送一條Lock前綴的指令,Lock前綴的指令在多核處理器下會引發兩件事情:
一致性協:每個處理器通過嗅探在總線上傳播的數據來檢查自己的緩存的值是否過期了,當處理器發現自己的緩存行對應的內存過期,在下次訪問相同內存地址時,強制執行緩存填充,從系統內存中讀取。
簡單理解:volatile在其修飾的變量被線程修改時,會強制其他線程在下一次訪問該變量時刷新緩存區。
volatile的兩條實現原則
小結
Lock前綴的指令會引起處理器緩存寫回內存;
一個處理器的緩存回寫到內存會導致其他處理器的緩存失效;
當處理器發現本地緩存失效后,就會從內存中重讀該變量數據,即可以獲取當前最新值。
volatile是如何保證有序性
在解釋有序性前,我們先來看看什么是指令重排?
導致程序有序性的原因是編譯優化,指令重排序是JVM為了優化指令,提高程序運行效率,在不影響單線程程序執行結果的前提下,盡可能地提高并行度。但是在多線程環境下,有些代碼的順序改變,有可能引發邏輯上的不正確。有序性最直接的辦法就是禁用緩存和編譯優化,但是這樣問題雖然解決了,我們的程序的性能就堪憂了,所以合理的方案是按需禁用緩存或者編譯優化。
接下來我們來看一個著名的單例模式雙重檢查鎖的實現
class Singleton { private volatile static Singleton instance = null; private Singleton() { } public static Singleton getInstance() { if (instance == null) { //步驟1 synchronized (Singleton.class) { if (instance == null) //步驟2 instance = new Singleton(); //步驟3 } } return instance; }}在以上代碼中,instance不用volatile修飾時,輸出的結果會是什么呢?我們的預期中代碼是這樣子執行的:線程A和B同時在調用getInstance()方法,線程A執行步驟1,發現instance為 null,然后同步鎖住Singleton類,接著執行步驟2再次判斷instance是否為null,發現仍然是null,然后執行步驟3,開始實例化Singleton。這樣看好像沒啥毛病,可是仔細一想,發現事情并不簡單。 這時候,我們來我們先了解一下對象是怎么初始化的?
- 對象在初始化的時候分三個步驟
程序為了優化性能,會將2和3進行重排序,此時執行的順序是1、3、2,在單線程中,對結果是不會有影響的,可是在多線程程序下,問題就暴露出來了。這時候我們回到剛剛的單例模式中,在實例化的過程中,線程B走到步驟1,發現instance不為空,但是有可能因為指令重排了,導致instance還沒有完全初始化,程序就出問題了。為了禁止實例化過程中的重排序,我們用volatile對instance修飾。
volatile內存語義如何實現
對于一般的變量則會被重排序(重排序分析編譯器重排序和處理器重排序),而對于volatile則不能,這樣會影響其內存語義,所以為了實現volatile的內存語義JMM會限制重排序。
其重排序規則如下:
volatile的底層實現是通過插入內存屏障,但是對于編譯器來說,發現一個最優布置來最小化插入內存屏障的總數幾乎是不可能的,所以,JMM采用了保守策略。如下:
在每一個volatile讀操作后面插入一個LoadLoad屏障,用來禁止處理器把上面的volatile讀與后面任意操作重排序
在每一個volatile寫操作前面插入一個StoreStore屏障,用來禁止volatile寫與前面任意操作重排序
在每一個volatile寫操作后面插入一個StoreLoad屏障,用來禁止volatile寫與后面可能有的volatile讀/寫操作重排序
在每一個volatile讀操作前面插入一個LoadStore屏障,用來禁止volatile寫與后面可能有的volatile讀/寫操作重排序
保守策略下,volatile的寫插入屏障后生成的指令示意圖:
Storestore 屏障可以保證在volatile寫之前,其前面的所有普通寫操作已經對任意處理器可見了,Storestore 屏障將保障上面所有的普通寫在volatile寫之前刷新到主內存。
這里比較有意思的是, volatite 寫后面的 StoreLoad 屏障的作用是避免volatile寫與后面可能有的volatile 讀/寫操作重排序。
因為編譯器常常無法準確判斷在一個volatile寫的后面是否需要插入一個StoreLoad屏障。為保證能正確實現volatile的內存語義,JMM在采取了保守策略,在每個volatile寫的后面,或者在每個 volatile讀的前面插入一個StoreLoad屏障。
保守策略下,volatile的讀插入屏障后生成的指令示意圖:
上面的內存屏障插入策略非常保守,在實際執行中,只要不改變volatile寫-讀的內存語義,編譯器可根據情況省略不必要的屏障
舉個例子:
public class Test { int a ; volatile int v1 = 1; volatile int v2 = 2; public void readWrite(){ int i = v1;//第一個volatile讀 int j = v2;//第二個volatile讀 a = i+j://普通讀 v1 = i+1;//第一個volatile寫 v2 =j+2;//第二個volatile寫 } public synchronized void read(){ if(flag){ System.out.println("---i = " + i); } }}針對readWrite方法,編譯器在生成字節碼的時候可以做到如下的優化:
注意:最后一個storeLoad屏障不能省略。因為第二個volatile寫之后,方法立即return,此時編譯器無法精準判斷后面是否會有vaolatile讀或者寫。
如何正確使用volatile變量
在某些情況下,如果讀操作遠遠大于寫操作,volatile 變量可以提供優于鎖的性能優勢。
可是volatile變量不是說用就能用的,它必須滿足兩個約束條件:
- 對變量的寫操作不依賴于當前值。
- 該變量沒有包含在具有其他變量的不變式中。
第一個條件的限制使volatile變量不能用作線程安全計數器。雖然 i++ 看上去類似一個單獨操作,實際上它是一個讀取-修改-寫入三個步驟的組合操作,必須以原子方式執行,而 volatile不能保證這種情況下的原子操作。正確的操作需要使i的值在操作期間保持不變,而volatile 變量無法做到這一點。
volatile和synchronized區別
volatile和atomic原子類區別
總結
總結一下volatile的特性``
- volatile可見性;對一個volatile的讀,總可以看到對這個變量最終的寫volatile有序性;JVM底層采用“內存屏障”來實現volatile語義volatile原子性;volatile對單個讀/寫具有原子性(32位Long、Double),但是復合操作除外,例如i++
如果你覺得文章還不錯,你的轉發、點贊、評論就是對我最大的鼓勵。感謝您的閱讀!
原創不易,歡迎轉發,求個關注!
總結
以上是生活随笔為你收集整理的内存位置访问无效_万字长文——java内存模型之volatile深入解读的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: web前端三大主流框架_小猿圈web前端
- 下一篇: 多线程处理同一批数据_多进程和多线程的优