高并发编程-通过volatile重新认识CPU缓存 和 Java内存模型(JMM)
文章目錄
- 概述
- volatile定義
- CPU緩存
- 相關CPU術語
- CPU緩存一致性協議MESI
- 帶有高速緩存的CPU執行計算的流程
- CPU 多級的緩存結構
- Java 內存模型 (JMM)
- 線程通信的兩種方式
- 哪些變量可以共享
- JMM概述
- Java內存模型的抽象結構示意圖
- volatile 小demo
- 總結:volatile的兩條實現原則
概述
在多線程并發編程中synchronized和volatile都扮演著重要的角色。 volatile是輕量級的 synchronized,它在高并發中保證了共享變量的“可見性”。
那什么是可見性呢?
可見性 我們可以理解為:當一個線修改一個共享變量時,另外一個線程能讀到這個修改的值。
如果volatile變量修飾符使用恰的話,它比synchronized的使用和執行成本更低,因為volatile不會引起線程上下文的切換和調度
volatile定義
Java規范第3版中對volatile的定義如下:Java允許線程訪問共享變量,為了確保共享變量能被準確和一致地更新,線程應該確保通過排他鎖單獨獲得這個變量。
Java提供了volatile關鍵字,在某些場景下volatile比鎖synchronized要更加方便。如果一個字段被聲明成volatile,Java線程內存模型(JMM)確保所有線程看到這個變量的值是一致的 .
CPU緩存
相關CPU術語
了解volatile實現原理之前,先了解下與其實現原理相關的CPU術語
| 內存屏障 | memory barriers | 一組處理器指令,用于實現對內存操作的順序限制 |
| 緩沖行 | cache line | 緩存中可以分配的最小存儲單位。處理器填寫緩存線時會加載整個緩存線,需要使用多個主內存讀周期; |
| 原子操作 | atomic operations | 不可中斷的一個或一系列的操作 |
| 緩存行填充 | cache line fill | 當處理器識別到從內存中讀取操作數是可緩存的,處理器讀取整個緩存行到適當的緩存; |
| 緩存命中 | cache hit | 如果進行高速緩存行填充操作的內存位置仍然是下次處理器訪問的地址時,處理器從緩存中讀取操作數,而不是從內存中讀取; |
| 寫命中 | write hit | 當處理器操作數寫回到一個內存緩存的區域時,它首先會檢查這個緩存的內存地址是否在緩存行中,如果存在一個有效的緩存行,則處理器將這個操作數寫回到緩存,而不是寫回到內存,這個操作被稱為寫命中; |
| 寫缺失 | write miss the cache | 一個有效的緩存行被寫入到不存在的內存區域。 |
CPU緩存一致性協議MESI
CPU緩存一致性協議MESI 請參考: CPU緩存一致性協議MESI
【M 修改 (Modified) E 獨享、互斥 (Exclusive) S 共享 (Shared) I 無效 (Invalid) 】
CPU的發展速度非常快,而內存和硬盤的發展速度遠遠不及CPU。這就造成了高性能能的內存和硬盤價格及其昂貴。然而CPU的高度運算需要高速的數據。為了解決這個問題,CPU廠商在CPU中內置了少量的高速緩存以解決I\O速度和CPU運算速度之間的不匹配問題
為了提高效率,CPU不直接和內存進行通信,而是先將系統內存的數據讀取到內部緩存(L1、L2或其他)后再進行操作。
但是有個問題: 當操作完成后,被修改的數據何時回寫到主內存呢?
假設某個共享變量聲明了volatile關鍵字進行寫操作 ,JVM就會向處理器發送一條Lock前綴指令,將這個變量所在緩存行的數據寫回到系統內存。
OK,就算寫回到內存,如果其他處理器緩存的值還是舊的,再執行計算操作就會有問題。
所以,在多處理器下,為了保證各個處理器的緩存是一致的,就要實現緩存一致性協議 ,每個處理器通過嗅探在總線(BUS)上傳播的數據來檢查自己緩存的值是不是過期了
- 當處理器發現自己緩存行對應的內存地址被修改,就會將當前處理器的緩存行設置成無效狀態,
- 當處理器對這個數據進行修改操作的時候,會重新從系統內存中把數據讀到處理器緩存
帶有高速緩存的CPU執行計算的流程
CPU 多級的緩存結構
由于CPU的運算速度超越了1級緩存的數據I\O能力,CPU廠商又引入了多級的緩存結構。
L1/L2/L3 Cache速度差別
L1 cache: 3 cycles
L2 cache: 11 cycles
L3 cache: 25 cycles
Main Memory: 100 cycles
通常L1 Cache離CPU核心需要數據的地方更近,而L2 Cache則處于邊緩位置,訪問數據時,L2 Cache需要通過更遠的銅線,甚至更多的電路,從而增加了延時。
參見: 細說Cache-L1/L2/L3/TLB
Java 內存模型 (JMM)
線程通信的兩種方式
我們知道 線程間的通信,主要分為兩種方式
哪些變量可以共享
Java的并發采用的是共享內存模型 , 在Java中,所有實例域、靜態域和數組元素都存儲在堆內存中,堆內存在線程之間共享 ,我們使用”共享變量”這個術語代指實例域,靜態域和數組元素
局部變量,方法定義參數和異常處理器參數不會在線程之間共享,它們不會有內存可見性問題,也不受內存模型的影響。
JMM概述
Java線程之間的通信由Java內存模型JMM)控制,JMM決定一個線程對共享變量的寫入何時對另一個線程可見 .
JMM定義了線程和主內存之間抽象關系:線程之間的共享變量存儲在主內存(Main Memory)中,每個線程都有一個私有的本地內存(Local Memory),本地內存中存儲了該線程以讀/寫共享變量的副本。
注: 本地內存是JMM的一個抽象概念,并不真實存在。它涵蓋了緩存、寫緩沖區、寄存器以及其他的硬件和編譯器化
Java內存模型的抽象結構示意圖
如下:
根據上述的描述,如果線程A和線程B要通信的話,步驟如下
線程A和線程B通信示意圖如下所示
本地內存A和本地內存B由主內存中共享變量x的副本。
從整體來看,這兩個步驟實質上是線程A在向線程B發送消息,而且這個通信過程必須要經過主內存。JMM通過控制主內存與每個線程的本地內存之間的交互,來保證內存可見性保證。
volatile 小demo
先來個例子 感受下volatile的作用
倆線程 1個讀取共享變量 另外1個更新共享變量.
package com.artisan.test;/*** 倆線程* <p>* 1個讀取共享變量* 1個更新共享變量*/ public class VolatileDemo {// 共享變量private volatile static int SHARED_VALUE = 0;private final static int MAX_VALUE = 10;public static void main(String[] args) {// 定義 讀取線程new Thread(() -> {int localValue = SHARED_VALUE;// 循環, 如果localValue != SHARED_VALUE 輸出信息while (SHARED_VALUE < MAX_VALUE){if (localValue != SHARED_VALUE){System.out.printf(Thread.currentThread().getName() + ": the SHARED_VALUE value has been updated to [%d] \n" , SHARED_VALUE);localValue = SHARED_VALUE;}}}, "Reader Thread").start();// 定義 更新線程new Thread(() -> {int localValue = SHARED_VALUE;// 循環 如果小于最大值,則更新localValuewhile (SHARED_VALUE < MAX_VALUE){System.out.printf(Thread.currentThread().getName() + ": update SHARED_VALUE to [%d] \n" , ++localValue);SHARED_VALUE = localValue;try {// 為了演示效果,休眠一下Thread.sleep(500);} catch (InterruptedException e) {e.printStackTrace();}}}, "Update Thread").start();} }輸出:
Update Thread: update SHARED_VALUE to [1] Reader Thread: the SHARED_VALUE value has been updated to [1] Update Thread: update SHARED_VALUE to [2] Reader Thread: the SHARED_VALUE value has been updated to [2] Update Thread: update SHARED_VALUE to [3] Reader Thread: the SHARED_VALUE value has been updated to [3] Update Thread: update SHARED_VALUE to [4] Reader Thread: the SHARED_VALUE value has been updated to [4] Update Thread: update SHARED_VALUE to [5] Reader Thread: the SHARED_VALUE value has been updated to [5] Update Thread: update SHARED_VALUE to [6] Reader Thread: the SHARED_VALUE value has been updated to [6] Update Thread: update SHARED_VALUE to [7] Reader Thread: the SHARED_VALUE value has been updated to [7] Update Thread: update SHARED_VALUE to [8] Reader Thread: the SHARED_VALUE value has been updated to [8] Update Thread: update SHARED_VALUE to [9] Reader Thread: the SHARED_VALUE value has been updated to [9] Update Thread: update SHARED_VALUE to [10] Process finished with exit code 0如果 去掉volatile關鍵字呢 ? 測試如下
由此可見 volatile關鍵字在高并發中保證了共享變量的“可見性”。
總結:volatile的兩條實現原則
總結一下
- Lock前綴指令會引起處理器緩存回寫到內存
- 一個處理器的緩存回寫到內存會導致其他處理器的緩存無效
總結
以上是生活随笔為你收集整理的高并发编程-通过volatile重新认识CPU缓存 和 Java内存模型(JMM)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 高并发编程-Wait Set 多线程的“
- 下一篇: 高并发编程-重新认识Java内存模型(J