volatile 與線程的那些事
volatile 關(guān)鍵字
如果一個(gè)變量在多個(gè)CPU中都存在緩存(一般在多線程編程時(shí)才會(huì)出現(xiàn)),那么就可能存在緩存不一致的問題。為了解決緩存不一致性問題,通常來說有以下2種解決方法:
通過在總線加LOCK#鎖的方式
通過緩存一致性協(xié)議
這2種方式都是硬件層面上提供的方式。
i如果在執(zhí)行這段代碼的過程中,在總線上發(fā)出了LCOK#鎖的信號(hào),那么只有等待這段代碼完全執(zhí)行完畢之后,其他CPU才能從變量i所在的內(nèi)存讀取變量,然后進(jìn)行相應(yīng)的操作。這樣就解決了緩存不一致的問題。
緩存一致性協(xié)議。最出名的就是Intel 的MESI協(xié)議,MESI協(xié)議保證了每個(gè)緩存中使用的共享變量的副本是一致的。它核心的思想是:當(dāng)CPU寫數(shù)據(jù)時(shí),如果發(fā)現(xiàn)操作的變量是共享變量,即在其他CPU中也存在該變量的副本,會(huì)發(fā)出信號(hào)通知其他CPU將該變量的緩存行置為無效狀態(tài),因此當(dāng)其他CPU需要讀取這個(gè)變量時(shí),發(fā)現(xiàn)自己緩存中緩存該變量的緩存行是無效的,那么它就會(huì)從內(nèi)存重新讀取。

Java內(nèi)存間交互操作
JLS定義了線程對(duì)主存的操作指令:lock,unlock,read,load,use,assign,store,write。這些行為是不可分解的原子操作,在使用上相互依賴,read-load從主內(nèi)存復(fù)制變量到當(dāng)前工作內(nèi)存,use-assign執(zhí)行代碼改變共享變量值,store-write用工作內(nèi)存數(shù)據(jù)刷新主存相關(guān)內(nèi)容。
read(讀取):作用于主內(nèi)存變量,把一個(gè)變量值從主內(nèi)存?zhèn)鬏數(shù)骄€程的工作內(nèi)存中,以便隨后的load動(dòng)作使用
load(載入):作用于工作內(nèi)存的變量,它把read操作從主內(nèi)存中得到的變量值放入工作內(nèi)存的變量副本中。
use(使用):作用于工作內(nèi)存的變量,把工作內(nèi)存中的一個(gè)變量值傳遞給執(zhí)行引擎,每當(dāng)虛擬機(jī)遇到一個(gè)需要使用變量的值的字節(jié)碼指令時(shí)將會(huì)執(zhí)行這個(gè)操作。
assign(賦值):作用于工作內(nèi)存的變量,它把一個(gè)從執(zhí)行引擎接收到的值賦值給工作內(nèi)存的變量,每當(dāng)虛擬機(jī)遇到一個(gè)給變量賦值的字節(jié)碼指令時(shí)執(zhí)行這個(gè)操作。
store(存儲(chǔ)):作用于工作內(nèi)存的變量,把工作內(nèi)存中的一個(gè)變量的值傳送到主內(nèi)存中,以便隨后的write的操作。
write(寫入):作用于主內(nèi)存的變量,它把store操作從工作內(nèi)存中一個(gè)變量的值傳送到主內(nèi)存的變量中。
原子性,可見性,有序性
多線程的三個(gè)特性。
對(duì)于可見性,Java提供了volatile關(guān)鍵字來保證可見性。
當(dāng)一個(gè)共享變量被volatile修飾時(shí),它會(huì)保證修改的值會(huì)立即被更新到主存,當(dāng)有其他線程需要讀取時(shí),它會(huì)去內(nèi)存中讀取新值。
而普通的共享變量不能保證可見性,因?yàn)槠胀ü蚕碜兞勘恍薷闹螅裁磿r(shí)候被寫入主存是不確定的,當(dāng)其他線程去讀取時(shí),此時(shí)內(nèi)存中可能還是原來的舊值,因此無法保證可見性。
另外,通過synchronized和Lock也能夠保證可見性,synchronized和Lock能保證同一時(shí)刻只有一個(gè)線程獲取鎖然后執(zhí)行同步代碼,并且在釋放鎖之前會(huì)將對(duì)變量的修改刷新到主存當(dāng)中。因此可以保證可見性。
下面這段話摘自《深入理解Java虛擬機(jī)》:
“觀察加入volatile關(guān)鍵字和沒有加入volatile關(guān)鍵字時(shí)所生成的匯編代碼發(fā)現(xiàn),加入volatile關(guān)鍵字時(shí),會(huì)多出一個(gè)lock前綴指令”
lock 前綴指令實(shí)際上相當(dāng)于一個(gè)內(nèi)存屏障(也成內(nèi)存柵欄),內(nèi)存屏障會(huì)提供3個(gè)功能:
它確保指令重排序時(shí)不會(huì)把其后面的指令排到內(nèi)存屏障之前的位置,也不會(huì)把前面的指令排到內(nèi)存屏障的后面;即在執(zhí)行到內(nèi)存屏障這句指令時(shí),在它前面的操作已經(jīng)全部完成;
它會(huì)強(qiáng)制將對(duì)緩存的修改操作立即寫入主存;
如果是寫操作,它會(huì)導(dǎo)致其他CPU中對(duì)應(yīng)的緩存行無效。
下面列舉幾個(gè)Java中使用volatile的幾個(gè)場景。
狀態(tài)標(biāo)記量
double check防止指令重排序:
防止指令重排序?qū)е缕渌€程獲取到未初始化完的對(duì)象。instance = new Singleton()這句,這并非是一個(gè)原子操作,事實(shí)上在 JVM 中這句話大概做了下面 3 件事情。但是在 JVM 的即時(shí)編譯器中存在指令重排序的優(yōu)化。也就是說上面的第二步和第三步的順序是不能保證的,最終的執(zhí)行順序可能是 1-2-3 也可能是 1-3-2。如果是后者,則在 3 執(zhí)行完畢、2 未執(zhí)行之前,被線程二搶占了,這時(shí) instance 已經(jīng)是非 null 了(但卻沒有初始化),所以線程二會(huì)直接返回 instance,然后使用,然后報(bào)錯(cuò)。加了volatile就不會(huì)重排序。
給 instance 分配內(nèi)存
調(diào)用 Singleton 的構(gòu)造函數(shù)來初始化成員變量
將instance對(duì)象指向分配的內(nèi)存空間(執(zhí)行完這步 instance 就為非 null 了)
當(dāng)new一個(gè)對(duì)象的時(shí)候,也是被分配在主內(nèi)存。
synchronized 關(guān)鍵字和 volatile 關(guān)鍵字的區(qū)別
volatile關(guān)鍵字是線程同步的輕量級(jí)實(shí)現(xiàn),所以volatile性能肯定比synchronized關(guān)鍵字要好。但是volatile關(guān)鍵字只能用于變量,而synchronized關(guān)鍵字可以修飾方法以及代碼塊。
多線程訪問volatile關(guān)鍵字不會(huì)發(fā)生阻塞,而synchronized關(guān)鍵字可能會(huì)發(fā)生阻塞。
volatile關(guān)鍵字能保證數(shù)據(jù)的可見性,但不能保證數(shù)據(jù)的原子性。synchronized關(guān)鍵字兩者都能保證。
volatile關(guān)鍵字主要用于解決變量在多個(gè)線程之間的可見性,而 synchronized關(guān)鍵字解決的是多個(gè)線程之間訪問資源的同步性
記得點(diǎn)「贊」和「在看」↓
愛你們
