都是緩存惹的禍
緩存有效的解決了速度不同的設備之間的訪問問題,但是它也帶來了更多的問題,今天介紹一個緩存引起的數據不一致問題以及對應的解決方法。
先給自己挖個坑,三部曲如下:
volatile 三部曲之可見性
volatile 三部曲之有序性
volatile 三部曲之經典應用
今天講可見性,廢話不多說,開始。
友情提示:本文基於 Java 語言,CPU 基於 x86 架構。
有一個內存,在其 0x400 位置處,存儲着數字 1
有一個處理器,從內存中讀數據到寄存器時,會將讀到的數據在緩存中存儲一份。
現在,這個處理器讀取到了三條機器指令,將內存中的數字改寫爲了 2
我們看到,這個寫的過程被細化成了兩步,需要先寫到處理器緩存,再從緩存刷新到內存。
同樣對於讀來說,也需要先讀緩存,如果讀不到再去內存中獲取,同時更新緩存。
這樣,對於單個處理器來說,由於緩存的存在,讀寫效率都有所提升。
可見性
===========
可是,如果有另一個處理器呢?
場景一:處理器 1 未及時將緩存中的值刷新到內存,導致處理器 2 讀到了內存中的舊值。
場景二:處理器 1 及時刷新緩存到了內存,但處理器 2 讀的是自己緩存中的舊值。
可以看到,這兩種場景,都是處理器 1 認爲,已經將共享變量改寫爲了 2,但處理器 2 讀到的值仍然是 1。
換句話說,處理器 1 對這個共享變量的修改,對處理器 2 來說 "不可見"。
現在我們加入線程的概念,假設線程 1 運行在處理器 1,線程 2 運行在處理器 2。
那麼就可以說:
線程 1 對這個共享變量的修改,對線程 2 來說 "不可見"。
這個問題,就被稱爲可見性問題。
LOCK
============
假如線程 1 對共享變量的修改,線程 2 立刻就能夠看到。
那麼就可以說,這個共享變量,具有可見性。
那如何做到這一點呢?
我們首先想想看,剛剛的兩個場景,爲什麼不可見。
1. 線程 1 對共享變量的修改,如果剛剛將其值寫入自己的緩存,卻還沒有刷新到內存,此時內存的值仍爲舊值。
2. 即使線程 1 將其修改後的值,從緩存刷新到了內存,但線程 2 仍然從自己的緩存中讀取,讀到的也可能是舊值。
所以,問題就出在這兩個地方。
那要解決這個問題也非常簡單,只需要在線程 1 將共享變量進行寫操作時,產生如下兩個效果即可。
1. 線程 1 將新值寫入緩存後,立刻刷新到內存中。
2. 這個寫入內存的操作,使線程 2 的緩存無效。若想讀取該共享變量,則需要重新從內存中獲取。
這樣,該共享變量,就具有了可見性。
那如何使得,一個線程在進行寫操作時,有上述兩個效果呢?
答案是 LOCK 指令。
假如,線程 1 執行了如下指令,將內存中某地址處的值 + 1。
1add [某內存地址], 1
2
現在這個寫操作,不會立即刷新到內存,也不會將其他處理器中的緩存失效,也即不具備可見性。
那隻需要加上一個 LOCK 前綴。
1lock add [某內存地址], 1
這樣,這個操作就會使得:
1. 立即將該處理器緩存(具體說是緩存行)中的數據刷新到內存。
2. 使得其他處理器緩存(具體說是緩存了該內存地址的緩存行)失效。
第一步將緩存刷新到內存後,使得其他處理器緩存失效,也就是第二步的發生,是利用了 CPU 的緩存一致性協議。
而爲了實現緩存一致性協議,每個處理器通常的一個做法是,通過監聽在總線上傳播的數據來判斷自己的緩存值是否過期,這種方式叫總線嗅探機制。
總之,這兩個效果一出,在程序員或者線程的眼中,就變成了可見性的保證。
JMM
===========
現在,讓我們來到 Java 語言的世界。
上面那些處理器、寄存器、緩存等,都是硬件層面的概念,如果把這些無聊的、難學的細節,暴露給程序員,估計 Java 就無法流行起來了吧。
Java 可不希望這種情況發生,於是發明了一個簡單的、抽象的內存模型,來屏蔽這些硬件層面的細節。
這個內存模型就叫做 JMM,Java Memory Module。
一個線程寫入一個共享變量時,需要先寫入自己的本地內存,再刷新到主內存。默認情況下,JMM 並不會保證什麼時候刷新到主內存。
同樣,一個線程讀一個共享變量時,需要先讀取自己的本地內存,如果讀不到再去主內存中讀取,同時更新到自己的本地內存。
有同學就要問了,這個本地內存,是在內存中開闢的一塊空間麼?一個線程讀一個內存中的數據,還需要從內存一個地方拷貝到另一個地方?
爲啥上面有個 ×?因爲怕有的人把這個圖當成正解了...
注意,JMM 是語言級的內存模型,所以你千萬不能把這個模型中的概念,同真實的硬件層的概念相關聯,這也是很多同學對此感到迷惑的根源。
JMM 的出現,就是爲了讓程序員不要去想硬件上的細節,但這樣的命名方式,反而使程序員理解起來更加困惑了。
如果非要對應硬件上的原理,那不準確地說,這裏的本地內存實際上在並不真實存在,是由於處理器中的緩存機制而產生的抽象概念。這麼說可能稍稍解決你的一點點困惑。
之所以說不準確,一是因爲處理器有很多不同的架構,並不一定所有的架構都有緩存。二是因爲除了緩存之外,還有其他硬件和編譯器的優化,可以導致本地內存這個概念的存在。
所以從某種程度上說,JMM 還確實是大大簡化和屏蔽了程序員對於硬件細節的瞭解。
volatile
================
根據 JMM 向程序員提供的抽象模型,我們可以推測出如下問題。
此時線程 2 並沒有讀到線程 1 寫入的最新值,a=2,而是讀到了主內存中的舊值,a=1。
也即,線程 1 對共享變量的寫入,對線程 2 不可見。
那麼在 Java 中,如何讓一個共享變量具有上述的可見性呢?
答案是加一個 volatile 即可。
在 jls 裏是這樣描述 volatile 的。
The Java programming language allows threads to access shared variables. As a rule, to ensure that shared variables are consistently and reliably updated, a thread should ensure that it has exclusive use of such variables by obtaining a lock that, conventionally, enforces mutual exclusion for those shared variables.
The Java programming language provides a second mechanism, volatile fields, that is more convenient than locking for some purposes.
簡單說,Java 語言爲了確保共享變量得到一致和可靠的更新,可以通過鎖,也可以通過更輕量的 volatile 關鍵字。
比如在一個變量 a 前面加上了 volatile 關鍵字
1volatile int a;
那麼在寫這個 volatile 變量時,JMM 會把該線程對應的本地內存中的共享變量值立即刷新到主內存。
相應地,當讀一個 volatile 變量時,JMM 會把該線程對應的本地內存置爲無效。線程接下來將從主內存中讀取共享變量。
以上兩點,就是 volatile 的內存語義。
而這兩點的實質上,是完成了一次線程間通信,即線程 1 向線程 2 發送了消息。
有的同學可能又要問了,內存語義,那真的是寫的時候刷新到主內存,而讀的時候讓本地內存失效麼?
這裏我還是要強調,JMM 是語言級的內存模型,無論它硬件層面上是怎麼去保證的,在你站在語言層面去學習 JMM 時,就不要去想硬件細節。
爲了解決部分同學的困惑,我還是用不準確的語言來說一下,volatile 的底層會被轉化成上面所說的 LOCK 指令,寫這個共享變量時,就既做了刷新到主內存,同時也將其他處理器緩存失效的操作,並不是寫的時候刷新緩存,讀的時候再去將本地內存失效。
但在語言層去描述 volatile 的內存語義時,剛剛的說法完全沒錯,只要程序員按照 JMM 這個內存模型和 volatile 的內存語義去編程,能夠方便理解,且能夠達到預期的效果,即可。至於是不是準確表達了硬件層面的原理,這個是不重要的。
這讓我想到了之前看過的一個演講,我記得叫 “眼見爲實”,是說我們看到的,並不一定是這個宇宙的真實面貌,只是能讓我們更好地生存並延續後代,而已。
寫這篇文章時真的是瑟瑟發抖,一是因爲網上講這個知識點的實在太多了,二是我發現 volatile 這個知識點水很深,從底層硬件一直到上層語言,每一層都有實現原理,層層抽象直到上層表現爲我們看到的樣子。
我甚至覺得不可能有人對這個知識點完全理解透徹。緩存一致性和總線嗅探,你需要了解 CPU 硬件的原理吧?JMM 內存模型,你需要了解 JVM 虛擬機實現吧?
或者不說實現的事兒,就單單是 JMM 說了什麼,很多人覺得懂了,但你看過 JSR133 文檔對 JMM 模型的正式規範麼?很長,給大家隨便截取一小段。
所以隨着不斷研究這個知識點,我發現我越來越不懂 volatile 了。
但我還是寫下了這篇文章,並給自己挖了個坑。
這篇文章我盡全力把網上一些混亂的概念講解,重新理清楚,且儘量把和可見性無關的東西去掉。
但我還是寫的很不滿意,也很鬱悶。
因爲我覺得,我離 volatile 的真相,還很遙遠。
=================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/7PgUa696U5pZFLEcbWHYSA