在盤 volatile 之前,我們先看一個例子
通過執行代碼,我們會看到,該程序執行進入了死循環。子線程修改了共享變量的值,主線程不能直接看到子線程修改後變量的最新值。
為什麼多線程下會出現共享變量的不可變性???
我們會從 JMM(Java Memory Mode) 裡分析
JMM 規範:
① 所有的共享變量(實例變量和類變量,不包括局部變量,局部變量是線程私有的)都存儲於【主內存】;
② 每個線程有自己的【工作內存】,保留了被線程使用的變量的【工作副本】;
③ 線程對變量的所有操作(讀、寫)在【工作內存】中完成,而不是直接在主內存中;
④ 不同線程之間不能直接訪問對方【工作內存】中的變量,線程間變量的值的傳遞需要通過【主內存】中轉完成。
某些情況下,線程的工作內存會刷新到主內存中。詳情可移步
https://www.toutiao.com/i6822144960742031879/
變量不可見性的解決方案
如何實現在多線程下訪問共享變量的可見性?
① 加鎖;② 對共享變量使用 volatile 關鍵字
1、加鎖
使用 synchronized 後,程序結束循環,輸出 i 的值。
為什麼加鎖可以使共享變量可見?
我們這裡簡單說下 synchronized 代碼塊執行過程:
① 線程獲取鎖
② 清空工作內存
③ 從主內存拷貝共享變量最新值到工作內存成為新的副本
④ 執行代碼
⑤ 將修改後的副本刷新回主內存中
⑥ 線程釋放鎖
2、使用 volatile 修飾共享變量
用 volatile 修飾的共享變量,保證了可見性。
它會保證修改的值會【立即】被更新到主存,當有其他線程需要讀取時,它會去內存中讀取新值。
從 JMM 中,我們可以看到多個處理器將高速緩存中的共享變量刷新到主內存中,需要遵循緩存一致性協議。
為了達到數據訪問的一致,需要各個處理器在訪問緩存時遵循一些協議,在讀寫時根據協議來操作,常見的協議有 MSI、MESI、MOSI 等。最常見的就是 MESI 協議:
MESI 協議
MESI 是緩存數據的 4 中狀態
① M (Modified):被修改的。處於這一狀態的數據,只在本 CPU 中有緩存數據,而其他 CPU 中沒有。同時其狀態相對於內存中的值來說,是已經被修改的,且沒有更新到內存中。
② E (Exclusive):獨佔的。處於這一狀態的數據,只有在本 CPU 中有緩存,且其數據沒有修改,即與內存中一致。
③ S (Shared):共享的。處於這一狀態的數據在多個 CPU 中都有緩存,且與內存一致。
④ I (Invalid):要麼已經不在緩存中,要麼它的內容已經過時。為了達到緩存的目的,這種狀態的段將會被忽略。一旦緩存段被標記為失效,那效果就等同於它從來沒被加載到緩存中。
嗅探
通過嗅探技術
① 處於 M 狀態的緩存行,必須時刻監聽所有試圖讀取該緩存行對應的主存地址的操作,如果監聽到,則必須在此操作執行前把其緩存行中的數據寫回 CPU。
② 處於 S 狀態的緩存行,必須時刻監聽使該緩存行無效或者獨享該緩存行的請求,如果監聽到,則必須把其緩存行狀態設置為 I。
③ 處於 E 狀態的緩存行,必須時刻監聽其他試圖讀取該緩存行對應的主存地址的操作,如果監聽到,則必須把其緩存行狀態設置為 S。
④ 只有 E 和 M 可以進行寫操作而且不需要額外操作,如果想對 S 狀態的緩存字段進行寫操作,那必須先發送一個 RFO (Request-For-Ownership) 廣播,該廣播可以讓其他 CPU 的緩存中的相同數據的字段實效,即變成 I 狀態。
通過以上機制可以使得處理器在每次讀寫操作都是原子的,並且每次讀到的數據都是最新的。
總線風暴
由於 volatile 的 mesi 緩存一致性協議需要不斷的從主內存嗅探和 cas 不斷循環無效交互導致總線帶寬達到峰值。
所以不要大量使用 Volatile,可以使用 synchronized 代替。
volatile 除了可見性,還有一個特性:禁止進行指令重排序。
指令重排序
① 編譯器優化的重排序,在不改變單線程語義的情況下重新安排語句的執行順序;
② 指指令級並行重排序,處理器的指令級並行技術將多條指令重迭執行,如果不存在數據的依賴性將會改變語句對應機器指令的執行順序;
③ 內存系統的重排序,因為使用了讀寫緩存區,使得看起來並不是順序執行的。
重排序的好處:
提高處理的速度
舉個例子
雖然重排序可以提高執行效率,但是在多線程併發下,可以出現問題。
在單例雙重鎖模式中,使用 volatile
Singleton singleton = new Singleton();
分三步指令:
① 分配內存地址;
② new 一個 Singleton 對象;
③ 將內存地址賦值給 inst;
CPU 為了提高執行效率,這三步操作的順序可以是 ①②③,也可以是 ①③②,如果是 ①③② 順序的話,當把內存地址賦給 inst 後,inst 對象的內存地址上面還沒有 new 出來單例對象,這時候,如果就拿到 inst 的話,它其實就是空的,會報空指針異常。這就是為什麼雙重檢查單例模式中,單例對象要加上 volatile 關鍵字。
volatile 如何保證不被重排序的???
內存屏障
Java 編譯器會在生成指令系列時在適當的位置會插入內存屏障指令來禁止特定類型的處理器重排序。
volatile 不能保證原子性
原子性:在一次操作或者多次操作中,要麼所有操作全部得到執行並且不會受到任何因素的干擾而中斷,要麼所有的操作都不執行,volatile 不保證原子性操作。
輸出結果:
i++ 操作包含 3 個步驟:
① 從主內存中讀取數據到工作內存;
② 對工作內存中的數據進行 ++ 操作;
③ 將工作內存中的數據寫到主內存
在多線程環境下, volatile 關鍵字可以保證共享數據的可見性,但是不能保證對數據操作的原子性。
使用 AtomicInteger 可以解決原子性操作
AtomicInteger 是一個支持原子操作的 Integer 類,它提供了原子自增方法、原子自減方法以及原子賦值方法等。其底層是通過 volatile 和 CAS 實現的,其中 volatile 保證了內存可見性,CAS 算法保證了原子性。
volatile 使用場景
① 適合純賦值操作,不適合做 i++ 寫操作
② 觸發器。
我們可以將某個變量設置為 volatile 修飾,當其他線程一旦發現該變量修改的值後,觸發獲取到的該變量之前的操作都是最新且可見的。
volatile 與 synchronized(以下簡稱“syn”) 的區別?
① volatile 只能修飾實例變量和類變量,而 syn 可以修飾方法以及代碼塊;
② voliate 保證了數據的可見性,但是不保證原子性,而 syn 是一種排他(互斥)的機制;
③ voliate 進制指令重排序,可以解決單例雙重檢查對象初始化代碼執行亂序問題;
④ voliate 可以看做輕量級的是syn ,volatile 不保證原子性。
歡迎關注 ,一個會點 Python 的 Java 程序員。文章如有問題,你倒是說啊,喜歡的話,一鍵三連。
| 文
閱讀更多 Python大星 的文章