多線程“可見性”保證——volatile的應用

1. 引言

volatile是輕量級的synchronized,它在多處理器開發中保證了共享變量的“可見行”,可見性的意思是,當一個線程修改一個共享變量,另一個線程能讀到這個修改的值。如果volatile變量修飾符使用恰當的話,它比synchronized的使用和執行成本更低,因爲它不會引起線程上下文切換和調度。

2. CPU緩存行

2.1 CPU常用術語

術語 英文 描述
內存屏障 memory barriers 是一組處理器指令,用於實現對內存操作的順序限制
緩存行 cache line CPU高速緩存中可以分配的最小存儲單位,緩存行是2的整數冪個連續字節,最常見的是64個字節,高速緩存中加載和修改數據都是以緩存行爲基本單位進行處理的
原子操作 atomic operations 不可中斷的一個或一系列操作
緩存行填充 cache line fill 將主存中的數據緩存到緩存行中
緩存命中 cache hit 處理器從緩存中讀取到數據,而不是從主存中讀取
寫命中 write hit 當處理器操作數寫回到一個內存緩存區域時,它首先會檢查這個緩存的內存地址是否在緩存行中,如果存在一個有效的緩存行,則處理器將這個操作數寫回到緩存,而不是寫回到內存,這個操作被稱爲寫命中

2.2 詳解CPU緩存行

如果要了解緩存,就必須要瞭解緩存的結構,以及多個CPU核心訪問緩存存在的一些問題和注意事項。
每個緩存裏面都是由緩存行組成的,緩存系統中是以緩存行(cache line)爲單位存儲的。緩存行是2的整數冪個連續字節,最常見的緩存行大小是64個字節。當多線程修改互相獨立的變量時,如果這些變量共享同一個緩存行,就會無意中影響彼此的性能,這就是僞共享。
在這裏插入圖片描述
需要注意,數據在緩存中不是以獨立的項來存儲的,cache是由緩存行組成的,通常是64字節(比較舊的處理器緩存行是32字節),並且它有效地引用主內存中的一塊地址。一個Java的long類型是8字節,因此在一個緩存行中可以存8個long類型的變量。
在這裏插入圖片描述

2.3 緩存行的帶來的好處

如果你訪問一個long數組,當數組中的一個值被加載到緩存中,它會額外加載另外7個。因此你能非常快地遍歷這個數組。事實上,你可以非常快速的遍歷在連續的內存塊中分配的任意數據結構。
因此如果你數據結構中的項在內存中不是彼此相鄰的(鏈表,我正在關注你呢),你將得不到免費緩存加載所帶來的優勢。並且在這些數據結構中的每一個項都可能會出現緩存未命中。

2.4 緩存行帶來的問題

不過,所有這種免費加載有一個弊端。設想你有兩個單獨並且是相連的long類型變量,我們稱它爲head,另一個稱它爲tail。現在,當你加載head到緩存的時候,很可能你也免費加載了tail。這樣就會帶來一個問題,當core1修改了head變量時,core2需要讀取tail變量時,由於緩存一致性協議,雖然core1對tail變量沒有任何修改,緩存行和主存中對應對內容都更新了,core2對應的緩存行已經失效,需要重新讀取。
請記住我們必須以整個緩存行作爲單位來處理(這是CPU的實現所規定的),不能只把head標記爲無效。如果兩個線程同時分別寫head和tail變量,那麼對性能對影響會更糟。
在這裏插入圖片描述
當然解決這個問題對方法,就是將變量對長度增加爲緩存行對長度。

3. volatile原理

如果對聲明瞭volatile的變量進行寫操作,JVM就會向處理器發送一條Lock前綴的指令,將這個變量所在的緩存行的數據寫回到系統內存,由於緩存一致性協議的影響,每個處理器都會檢查自己的緩存是否過期,當處理器發現自己緩存行對應的內存地址被修改,就會將當前處理器的緩存行設置成無效狀態,當處理器對這個數據進行修改操作的時候,會重新從系統內存中把數據讀到處理器緩存裏。
總結起來就有以下兩點:

  1. Lock前綴指令會引起處理器緩存回寫到內存
  2. 一個處理器的緩存回寫到內存會導致其他處理器的緩存無效

4. volatile使用優化

爲了不使多個變量同時在一個緩存行中,可以將變量的字節數轉換爲與緩存行的字節數相等,這樣一個緩存行就只能存儲一個變量,避免了僞共享對性能對影響,但是如果變量不會被頻繁的讀寫,其實引起多個變量在同一個緩存行中衝突的概率會比較小,這樣做的意義也不是很大。

部分內容參考:https://www.jianshu.com/p/e338b550850f

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