Java volatile关键字原理解剖
Java volatile關鍵字原理解剖
文章目錄
- Java volatile關鍵字原理解剖
- 參考文章
- 前置知識
- CPU緩存模型
- CPU緩存行
- 并發編程基本概念
- Java鎖概念
- volatile關鍵字原理(主題)
- volatile特性
- volatile原理
參考文章
文章內容參考以下博客,并對其中volatile關鍵原理進行提煉,從各方面出發解剖volatile關鍵字。
[1] https://mp.weixin.qq.com/s/bm3VVYp_r2vWLiIUFpC-4g(Java技術迷公眾號)
[2] (2條消息) Java volatile關鍵字最全總結:原理剖析與實例講解(簡單易懂)_老鼠只愛大米的博客-CSDN博客_java volatile
前置知識
CPU緩存模型
因為內存和CPU之間存在速度差異,CPU使用三級高速緩存來平衡內存和CPU之前的速度差。L1,L2,L3 高速緩存集成到 CPU,L0 也就是寄存器,寄存器離 CPU 最近,訪問速度也最快,基本沒有時延。
大家可以打開window的任務管理器,點擊性能可以查看到CPU的情況,從下面的圖中我們可以得知,我這個CPU有8個物理處理器(8核CPU),16個邏輯處理器(8核16線程)。還能分析高速緩存L1,L2,L3分別為512KB,4.0MB,8.0MB。
這里需要介紹一下CPU的物理核心和邏輯核心分別代表什么,CPU的物理核心代表一個CPU的處理單元的個數,比如: CPU 包含 4 個物理核心 8 個邏輯核心(4核8線程)。4 個物理核心表示在同一時間可以允許 4 個線程并行執行。而邏輯核心代表的是:處理器利用超線程的技術將一個物理核心模擬出了兩個邏輯核心。
一個物理核心在同一時間只會執行一個線程,而超線程芯片可以做到線程之間快速切換,當一個線程在訪問內存的空隙,超線程芯片可以馬上切換去執行另外一個線程。因為切換速度非常快,所以在效果上看到是 8 個線程在同時執行。(引用文章【1】)
CPU緩存模型如下圖:
可以看到L3是多核共用的,而L2,L1是屬于CPU核心獨立占有的。我們可以在Linux系統中在/sys/devices/system/cpu/目錄下看多CPU設備的描述信息。該目錄下有多少個cpux就代表有多少個邏輯核心。
假設我們進入第一個邏輯核心:/sys/devices/system/cpu/cpu0/cache,會發現一下目錄:
- index0 描述L1Cache中DataCache 的信息
- index1 描述L1Cache 中 Instruction Cache 的信息
- index2 描述L2Cache 的信息
- index3 描述L3Cache 的信息
進入每個index目錄,每個目錄都會有以下部分或者全部的文件,分別為:
- level:表示該 cache 信息屬于哪一級,1 表示 L1Cache,以其類推
- type:表示屬于 L1Cache 的 DataCache;
- size:表示 DataCache 的大小為 32K;
- shared_cpu_list:之前我們提到 L1Cache 和 L2Cache 是 CPU 物理核所私有的,而由物理核模擬出來的邏輯核是共享 L1Cache 和 L2Cache 的,/sys/devices/system/cpu/ 目錄下描述的信息是邏輯核。shared_cpu_list 描述的正是哪些邏輯核共享這個物理核。
- coherency_line_size:該cache塊使用的緩存行大小
CPU緩存行
CPU 的高速緩存結構,引入高速緩存的目的在于消除 CPU 與內存之間的速度差距。數據在 CPU 高速緩存中的存取并不是以單獨的變量或者單獨的指針為單位存取的。而是以緩存行為存取單位。
CPU 高速緩存中存取數據的基本單位叫做緩存行 cache line。緩存行存取字節的大小為 2 的倍數,在不同的機器上,緩存行的大小范圍在 32 字節到 128 字節之間。目前所有主流的處理器中緩存行的大小均為 64 字節
一般現在的計算機CPU基本都是64字節為大小的存儲行,這也就意味著每次 CPU 從內存中獲取數據或者寫入數據的大小為 64 個字節,即使你只讀一個 bit,CPU 也會從內存中加載 64 字節數據進來。
比如你訪問一個 long 型數組,當 CPU 去加載數組中第一個元素時也會同時將后邊的 7 個元素一起加載進緩存中。這樣一來就加快了遍歷數組的效率。
long 類型在 Java 中占用 8 個字節,一個緩存行可以存放 8 個 long 型變量。
事實上,你可以非常快速的遍歷在連續的內存塊中分配的任意數據結構,如果你的數據結構中的項在內存中不是彼此相鄰的(比如鏈表),這樣就無法利用 CPU 緩存的優勢。由于數據在內存中不是連續存放的,所以在這些數據結構中的每一個項都可能會出現緩存行未命中(程序局部性原理)的情況。
Netty 利用數組實現的自定義 SelectedSelectionKeySet 類型替換掉了 JDK 利用 HashSet 類型實現的 sun.nio.ch.SelectorImpl#selectedKeys。目的就是利用 CPU 緩存的優勢來提高 IO 活躍的 SelectionKeys 集合的遍歷性能。(引用文章【1】)
并發編程基本概念
并發編程三大基本概念:
- 原子性:即一個操作或者多個操作 要么全部執行并且執行的過程不會被任何因素打斷,要么就都不執行。
- 可見性:指當多個線程訪問同一個變量時,一個線程修改了這個變量的值,其他線程能夠立即看得到修改的值。
- 有序性:即程序執行的順序按照代碼的先后順序執行。
原子性: jave關于并發編程原子性的包有:java.concurrent.Atomic.* 包,該包下的一切方法都是符合原子性的,如何描述原子性,一個很經典的例子就是銀行賬戶轉賬問題。假設:從賬戶A向賬戶B轉1000元,那么比如會切分為兩個操作,分別是:(1).從賬戶A減去1000元 (2).往賬戶B加上1000元,如果要保證原子性的話,那么這兩個操作要么全部成功,要么全部失敗,不可以存在(1)成功(2)失敗,反之也不可。
可見性:Java提供了volatile來保證可見性,當一個變量被volatile修飾后,表示著線程本地內存無效,當一個線程修改共享變量后他會立即被更新到主內存中,其他線程讀取共享變量時,會直接從主內存中讀取。當然,synchronize和Lock都可以保證可見性。synchronized和Lock能保證同一時刻只有一個線程獲取鎖然后執行同步代碼,并且在釋放鎖之前會將對變量的修改刷新到主存當中。因此可以保證可見性。(引用文章【2】)
有序性:在Java內存模型中,為了效率是允許編譯器和處理器對指令進行重排序。Java內存模型中的有序性可以總結為:如果在本線程內觀察,所有操作都是有序的;如果在一個線程中觀察另一個線程,所有操作都是無序的。前半句是指“線程內表現為串行語義”,后半句是指“指令重排序”現象和“工作內存主主內存同步延遲”現象。(引用文章【2】)
Java鎖概念
這里直接給我另一篇博客的鏈接,該博客詳細地介紹了鎖的概念和,Java中鎖的實現的介紹。(2條消息) JAVA鎖_鴨梨的藥丸哥的博客-CSDN博客
volatile關鍵字原理(主題)
volatile特性
在前面介紹過CPU的緩存模型,CPU的緩存行和并發編程基礎概念。下面的講述的volatile關鍵字原理與這三者的關系十分密切。
volatile擁有以下特性:
- 保證可見性:volatile變量會把該線程本地內存中的變量強制刷新到主內存中去,并且會讓其他線程中的volatile變量緩存無效。
- 禁止CPU指令重排:阻止CPU指令重排,確保程序按照代碼的先后順序執行
volatile原理
以下內容參考或直接引用(引用文章【1】)
假設我們現在定義一個類FalseSharding,FalseSharding代碼如下,字段 a,b 之間邏輯上是獨立的,它們之間一點關系也沒有,分別用來存儲不同的數據,數據之間也沒有關聯。
public class FalseSharding {volatile long a;volatile long b; }根據CPU緩存行介紹,我們可以得知我們一般CPU的緩存行大小為64字節,而字段 a,b 總共16字節。所以字段 a,b 有可能同時存在一個緩存行中。
如果恰好字段a,b 被 CPU 讀進了同一個緩存行,而此時有兩個線程,線程a用來修改字段a,同時線程b用來讀取字段 b。那么就會出現下面這種情況:
為了解決緩存不一致性問題,volatile使用以下2種解決方法:
- 通過在總線加LOCK鎖的方式(Lock前綴指令)
- 通過緩存一致性協議(Intel 的MESI協議)
Lock前綴指令和緩存一致性協議介紹如下:
- Lock 前綴指令可以使修改線程所在的處理器中的相應緩存行數據被修改后立馬刷新回內存中,并同時鎖定所有處理器核心中緩存了該修改變量的緩存行,防止多個處理器核心并發修改同一緩存行;
- 緩存一致性協議主要是用來維護多個處理器核心之間的 CPU 緩存一致性以及與內存數據的一致性。每個處理器會在總線上嗅探其他處理器準備寫入的內存地址,如果這個內存地址在自己的處理器中被緩存的話,就會將自己處理器中對應的緩存行置為無效,下次需要讀取的該緩存行中的數據的時候,就需要訪問內存獲取。
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-lyyHxeoJ-1649253305976)(F:\筆記文檔\筆記圖片\4.jpg)]
根據Lock前綴指令和緩存一致性協議,在volatile標識的數據可能會出現以下這兩者情況:
第一種情況
- 當線程 a 在處理器 core0 中對字段 a 進行修改時,Lock 前綴指令會將所有處理器中緩存了字段 a 的對應緩存行進行鎖定,這樣就會導致線程 b 在處理器 core1 中無法讀取和修改自己緩存行的字段 b;
- 處理器 core0 將修改后的字段 a 所在的緩存行刷新回內存中。
第二種情況
- 當處理器 core0 將字段 a 所在的緩存行刷新回內存的時候,處理器 core1 會在總線上嗅探到字段 a 的內存地址正在被其他處理器修改,所以將自己的緩存行置為失效。
- 當線程 b 在處理器 core1 中讀取字段b的值時,發現緩存行已被置為失效,core1 需要重新從內存中讀取字段 b 的值即使字段b沒有發生任何變化。
0 將字段 a 所在的緩存行刷新回內存的時候,處理器 core1 會在總線上嗅探到字段 a 的內存地址正在被其他處理器修改,所以將自己的緩存行置為失效。
- 當線程 b 在處理器 core1 中讀取字段b的值時,發現緩存行已被置為失效,core1 需要重新從內存中讀取字段 b 的值即使字段b沒有發生任何變化。
總結
以上是生活随笔為你收集整理的Java volatile关键字原理解剖的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 长安汽车首个海外生产基地在泰国动工,设计
- 下一篇: 刘强东回应采销喊话:水龙头已换新 还买了