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