什麼是內存屏障(Memory Barrier)以及在java中的應用

1. 指令重排序

程序在運行時內存實際的訪問順序和程序代碼編寫的訪問順序不一定一致,這就是內存亂序訪問。內存亂序訪問行爲出現的理由是爲了提升程序運行時的性能。這種內存亂序問題主要是由兩種原因引起的:

  • 編譯器在編譯時進行了編譯優化,導致指令重排;
  • 在多cpu環境下,爲了儘可能地避免處理器訪問主內存的時間開銷,處理器大多會利用緩存(cache)以提高性能。在這種模型下會存在一個現象,即緩存中的數據與主內存的數據並不是實時同步的,各CPU(或CPU核心)間緩存的數據也不是實時同步的。這導致在同一個時間點,各CPU所看到同一內存地址的數據的值可能是不一致的。從程序的視角來看,就是在同一個時間點,各個線程所看到的共享變量的值可能是不一致的。

爲什麼需要內存屏障
我們知道,在多CPU(核)場景下,爲了充分利用CPU,會通過流水線將指令並行進行。爲了能並行執行,又需要將指令進行重排序以便進行並行執行,那麼問題來了,那些指令不是在所有場景下都能進行重排,除了本身的一些規則(如Happens Before 規則)之外,我們還需要確保多CPU的高速緩存中的數據與內存保持一致性, 不能確保內存與CPU緩存數據一致性的指令也不能重排,內存屏障正是通過阻止屏障兩邊的指令重排序來避免編譯器和硬件的不正確優化而提出的一種解決辦法

 

2. java 內存模型中的happen before原則

JSR-1337制定了Java內存模型(Java Memory Model, JMM)中規定的hb原則大致有以下幾點:

  • 程序次序法則:線程中的每個動作A都happens-before於該線程中的每一個動作B,其中,在程序中,所有的動作B都能出現在A之後。
  • 監視器鎖法則:對一個監視器鎖的解鎖 happens-before於每一個後續對同一監視器鎖的加鎖。
  • volatile變量法則:對volatile域的寫入操作happens-before於每一個後續對同一個域的讀寫操作。
  • 線程啓動法則:在一個線程裏,對Thread.start的調用會happens-before於每個啓動線程的動作。
  • 線程終結法則:線程中的任何動作都happens-before於其他線程檢測到這個線程已經終結、或者從Thread.join調用中成功返回,或Thread.isAlive返回false。
  • 中斷法則:一個線程調用另一個線程的interrupt happens-before於被中斷的線程發現中斷。
  • 終結法則:一個對象的構造函數的結束happens-before於這個對象finalizer的開始。
  • 傳遞性:如果A happens-before於B,且B happens-before於C,則A happens-before於C

jmm 對java語義的比較重要的兩個擴展是:

  • 對volatile語義的擴展保證了volatile變量在一些情況下不會重排序,volatile的64位變量double和long的讀取和賦值操作都是原子的。
  • 對final語義的擴展保證一個對象的構建方法結束前,所有final成員變量都必須完成初始化(前提是沒有this引用溢出)。

3. 內存屏障(Memory Barrier)

  • Memory barrier 能夠讓 CPU 或編譯器在內存訪問上有序。一個 Memory barrier 之前的內存訪問操作必定先於其之後的完成。
  • Memory barrier是一種CPU指令,用於控制特定條件下的重排序和內存可見性問題。Java編譯器也會根據內存屏障的規則禁止重排序。
  • 有的處理器的重排序規則較嚴,無需內存屏障也能很好的工作,Java編譯器會在這種情況下不放置內存屏障。

Memory Barrier可以被分爲以下幾種類型:

  • LoadLoad屏障:對於這樣的語句Load1; LoadLoad; Load2,在Load2及後續讀取操作要讀取的數據被訪問前,保證Load1要讀取的數據被讀取完畢。
  • StoreStore屏障:對於這樣的語句Store1; StoreStore; Store2,在Store2及後續寫入操作執行前,保證Store1的寫入操作對其它處理器可見。
  • LoadStore屏障:對於這樣的語句Load1; LoadStore; Store2,在Store2及後續寫入操作被刷出前,保證Load1要讀取的數據被讀取完畢。
  • StoreLoad屏障:對於這樣的語句Store1; StoreLoad; Load2,在Load2及後續所有讀取操作執行前,保證Store1的寫入對所有處理器可見。它的開銷是四種屏障中最大的。在大多數處理器的實現中,這個屏障是個萬能屏障,兼具其它三種內存屏障的功能

參考https://www.cnblogs.com/chenyangyao/p/5269622.html可以得知:

  • Oracle的JDK中提供了Unsafe. putOrderedObject,Unsafe. putOrderedInt,Unsafe. putOrderedLong這三個方法,JDK會在執行這三個方法時插入StoreStore內存屏障,避免發生寫操作重排序。而在Intel 64/IA-32架構下,StoreStore屏障並不需要,Java編譯器會將StoreStore屏障去除。比起寫入volatile變量之後執行StoreLoad屏障的巨大開銷,採用這種方法除了避免重排序而帶來的性能損失以外,不會帶來其它的性能開銷。具體用法可以看下disruptor的Sequence的set方法。
  • Intel 64/IA-32架構下寫操作之間不會發生重排序,也就是說在處理器上操作的順序是可以保證的,這時候使用volatile來避免重排序是多此一舉的。但是,Java編譯器卻可能生成重排序後的指令。採用putOrderedObject可以解決這個問題。
  • 即使在其它會發生寫寫重排序的處理器中,由於StoreStore屏障的性能損耗小於StoreLoad屏障,採用這一方法也是一種可行的方案。但值得再次注意的是,這一方案不是對volatile語義的等價替換,而是在特定場景下做的特殊優化,它僅避免了寫寫重排序,但不保證內存可見性。

4. volatile語義中的內存屏障

  • 在每個volatile寫操作前插入StoreStore屏障,在寫操作後插入StoreLoad屏障;
  • 在每個volatile讀操作前插入LoadLoad屏障,在讀操作後插入LoadStore屏障;

volatile的內存屏障策略非常嚴格保守,保證了線程可見性。

5. final語義中的內存屏障

  • 新建對象過程中,構造體中對final域的初始化寫入(StoreStore屏障)和這個對象賦值給其他引用變量,這兩個操作不能重排序;
  • 初次讀包含final域的對象引用和讀取這個final域(LoadLoad屏障),這兩個操作不能重排序;
  • Intel 64/IA-32架構下寫操作之間不會發生重排序StoreStore會被省略,這種架構下也不會對邏輯上有先後依賴關係的操作進行重排序,所以LoadLoad也會變省略。

常見的應用場景

(1)通過 Synchronized關鍵字包住的代碼區域,當線程進入到該區域讀取變量信息時,保證讀到的是最新的值.這是因爲在同步區內對變量的寫入操作,在離開同步區時就將當前線程內的數據刷新到內存中,而對數據的讀取也不能從緩存讀取,只能從內存中讀取,保證了數據的讀有效性.這就是插入了StoreStore屏障;
(2)使用了volatile修飾變量,則對變量的寫操作,會插入StoreLoad屏障;
(3)其餘的操作,則需要通過Unsafe這個類來執行;
 

參考

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章