日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

内存位置访问无效_万字长文——java内存模型之volatile深入解读

發布時間:2024/10/14 编程问答 127 豆豆
生活随笔 收集整理的這篇文章主要介紹了 内存位置访问无效_万字长文——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性質

  • 保證了不同線程對這個變量進行操作時的可見性,即一個線程修改了某個變量的值,這新值對其他線程來說是立即可見的。(實現可見性)
  • 禁止進行指令重排序。(實現有序性)
  • 只能保證對單次讀/寫的原子性。i++ 這種操作不能保證原子性。(不能實現原子性)
  • 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前綴的指令在多核處理器下會引發兩件事情:

  • 將當前處理器緩存行的數據寫回到系統內存
  • 這個寫回內存的操作會使在其他CPU緩存了該內存地址的數據無效,保證各個處理器的緩存是一致的 (通過一致性協議來實現的)
  • 一致性協:每個處理器通過嗅探在總線上傳播的數據來檢查自己的緩存的值是否過期了,當處理器發現自己的緩存行對應的內存過期,在下次訪問相同內存地址時,強制執行緩存填充,從系統內存中讀取。

    簡單理解:volatile在其修飾的變量被線程修改時,會強制其他線程在下一次訪問該變量時刷新緩存區。

    volatile的兩條實現原則

  • Look 前綴指令會引起處理器緩存回寫到內存。Lock 前綴指令導致在執行指令期間,聲言處理器的LOCK#信號。在多處理器環境中,LOCK#信號確保在聲言該信號期間,處理器可以獨占任何共享內存。但是,在最近的處理器里,LOCK#信號一般不鎖總線,而是鎖緩存,畢竟鎖總線開銷的比較大,對干intel486和Pentiuln處理器,在鎖操作時,總是在總線上聲言LOCK#信號。但在P6和目前的處理器中,如果訪問的內存區域已經緩存在處理器內部,則不會聲言LOCK#信號。相反,它會鎖定這塊內存區域的緩存并回寫到內存,并使用緩存一致性機制來確保修改的原子性,此操作被稱為“緩存鎖定”,緩存一致性機制會阻止同時修改由兩個以上處理器緩存的內存區域數據。
  • 一個處理器的緩存回寫到內存會導致其他處理器的緩存無效。IA-32 處理器和 Iniel 64 處理器使用 MESI (修改、獨占、共享、無效)控制協議去維護內部緩存和其他處理器緩存的一致性。在多核處理器系統中進行操作的時候, IA-32和Intel64處理器能嗅探其他處理器訪問系統內存和它們的內部緩存。處理器使用嗅探技術保證它的內部緩存、系統內存和其他處理器的緩存的數據在總線上保持一致。例如,在 Pentium 和 P6famaly 處理器中,如果通過嗅探一個處理器來檢測其他處理器打算寫內存地址,而這個地址當前處干共享狀態,那么正在嗅探的處理器將使它的緩存行無效,在下次訪問相同內存地址時,強制執行緩存行填充
  • 小結

    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。這樣看好像沒啥毛病,可是仔細一想,發現事情并不簡單。 這時候,我們來我們先了解一下對象是怎么初始化的?

    • 對象在初始化的時候分三個步驟
    memory = allocate(); //1、分配對象的內存空間ctorInstance(memory); //2、初始化對象instance = memory; //3、使instance指向對象的內存空間

    程序為了優化性能,會將2和3進行重排序,此時執行的順序是1、3、2,在單線程中,對結果是不會有影響的,可是在多線程程序下,問題就暴露出來了。這時候我們回到剛剛的單例模式中,在實例化的過程中,線程B走到步驟1,發現instance不為空,但是有可能因為指令重排了,導致instance還沒有完全初始化,程序就出問題了。為了禁止實例化過程中的重排序,我們用volatile對instance修飾。

    volatile內存語義如何實現

    對于一般的變量則會被重排序(重排序分析編譯器重排序和處理器重排序),而對于volatile則不能,這樣會影響其內存語義,所以為了實現volatile的內存語義JMM會限制重排序。

    其重排序規則如下:
  • 如果第一個操作為volatile讀,則不管第二個操作是啥,都不能重排序。這個操作確保volatile讀之后的操作不會被編譯器重排序到volatile讀之前,其前面的所有普通寫操作都已經刷新到主內存中;
  • 如果第一個操作volatile寫,不管第二個操作是volatile讀/寫,禁止重排序。
  • 如果第二個操作為volatile寫時,則不管第一個操作是啥,都不能重排序。這個操作確保volatile寫之前的操作不會被編譯器重排序到volatile寫之后;
  • 如果第二個操作為volatile讀時,不管第二個操作是volatile讀/寫,禁止重排序
  • 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比synchronized執行成本更低,因為它不會引起線程上下文的切換和調度
  • volatile本質是在告訴jvm當前變量在寄存器(工作內存)中的值是不確定的,需要從主存中讀取;synchronized則是鎖定當前變量,只有當前線程可以訪問該變量,其他線程被阻塞住。
  • volatile只能用來修飾變量,而synchronized可以用來修飾變量、方法、和類。
  • volatile可以實現變量的可見性,禁止重排序和單次讀/寫的原子性;而synchronized則可以變量的可見性,禁止重排序和原子性。
  • volatile不會造成線程的阻塞;synchronized可能會造成線程的阻塞。
  • volatile標記的變量不會被編譯器優化;synchronized標記的變量可以被編譯器優化。
  • volatile和atomic原子類區別

  • Volatile變量可以確保先行關系,即寫操作會發生在后續的讀操作之前
  • 但是Volatile對復合操作不能保證原子性。例如用volatile修飾i變量,那么i++操作就不是原子性的。
  • atomic原子類提供的atomic方法可以讓i++這種操作具有原子性,如getAndIncrement()方法會原子性的進行增量操作把當前值加一,其它數據類型和引用變量也可以進行相似操作,但是atomic原子類一次只能操作一個共享變量,不能同時操作多個共享變量。
  • 總結

    總結一下volatile的特性``

    • volatile可見性;對一個volatile的讀,總可以看到對這個變量最終的寫volatile有序性;JVM底層采用“內存屏障”來實現volatile語義volatile原子性;volatile對單個讀/寫具有原子性(32位Long、Double),但是復合操作除外,例如i++

    如果你覺得文章還不錯,你的轉發、點贊、評論就是對我最大的鼓勵。感謝您的閱讀!

    原創不易,歡迎轉發,求個關注!

    總結

    以上是生活随笔為你收集整理的内存位置访问无效_万字长文——java内存模型之volatile深入解读的全部內容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。