新聞中心
這篇文章主要介紹“如何理解Java中volatile關(guān)鍵字”,在日常操作中,相信很多人在如何理解Java中volatile關(guān)鍵字問(wèn)題上存在疑惑,小編查閱了各式資料,整理出簡(jiǎn)單好用的操作方法,希望對(duì)大家解答”如何理解Java中volatile關(guān)鍵字”的疑惑有所幫助!接下來(lái),請(qǐng)跟著小編一起來(lái)學(xué)習(xí)吧!
在云和等地區(qū),都構(gòu)建了全面的區(qū)域性戰(zhàn)略布局,加強(qiáng)發(fā)展的系統(tǒng)性、市場(chǎng)前瞻性、產(chǎn)品創(chuàng)新能力,以專注、極致的服務(wù)理念,為客戶提供做網(wǎng)站、成都網(wǎng)站建設(shè) 網(wǎng)站設(shè)計(jì)制作按需制作,公司網(wǎng)站建設(shè),企業(yè)網(wǎng)站建設(shè),高端網(wǎng)站設(shè)計(jì),營(yíng)銷型網(wǎng)站建設(shè),外貿(mào)網(wǎng)站制作,云和網(wǎng)站建設(shè)費(fèi)用合理。
CPU
緩存
緩存模型
計(jì)算機(jī)中的所有運(yùn)算操作都是由CPU
完成的,CPU
指令執(zhí)行過(guò)程需要涉及數(shù)據(jù)讀取和寫入操作,但是CPU
只能訪問(wèn)處于內(nèi)存中的數(shù)據(jù),而內(nèi)存的速度和CPU
的速度是遠(yuǎn)遠(yuǎn)不對(duì)等的,因此就出現(xiàn)了緩存模型,也就是在CPU
和內(nèi)存之間加入了緩存層。一般現(xiàn)代的CPU
緩存層分為三級(jí),分別叫L1
緩存、L2
緩存和L3
緩存,簡(jiǎn)略圖如下:
L1
緩存:三級(jí)緩存中訪問(wèn)速度最快,但是容量最小,另外L1
緩存還被劃分成了數(shù)據(jù)緩存(L1d
,data
首字母)和指令緩存(L1i
,instruction
首字母)L2
緩存:速度比L1
慢,但是容量比L1
大,在現(xiàn)代的多核CPU
中,L2
一般被單個(gè)核獨(dú)占L3
緩存:三級(jí)緩存中速度最慢,但是容量最大,現(xiàn)代CPU
中也有L3
是多核共享的設(shè)計(jì),比如zen3
架構(gòu)的設(shè)計(jì)
緩存的出現(xiàn),是為了解決CPU
直接訪問(wèn)內(nèi)存效率低下的問(wèn)題,CPU
進(jìn)行運(yùn)算的時(shí)候,將需要的數(shù)據(jù)從主存復(fù)制一份到緩存中,因?yàn)榫彺娴脑L問(wèn)速度快于內(nèi)存,在計(jì)算的時(shí)候只需要讀取緩存并將結(jié)果更新到緩存,運(yùn)算結(jié)束再將結(jié)果刷新到主存,這樣就大大提高了計(jì)算效率,整體交互圖簡(jiǎn)略如下:
緩存一致性問(wèn)題
雖然緩存的出現(xiàn),大大提高了吞吐能力,但是,也引入了一個(gè)新的問(wèn)題,就是緩存不一致。比如,最簡(jiǎn)單的一個(gè)i++
操作,需要將內(nèi)存數(shù)據(jù)復(fù)制一份到緩存中,CPU
讀取緩存值并進(jìn)行更新,先寫入緩存,運(yùn)算結(jié)束后再將緩存中新的刷新到內(nèi)存,具體過(guò)程如下:
讀取內(nèi)存中的
i
到緩存中CPU
讀取緩存i
中的值對(duì)
i
進(jìn)行加1操作將結(jié)果寫回緩存
再將數(shù)據(jù)刷新到主存
這樣的i++
操作在單線程不會(huì)出現(xiàn)問(wèn)題,但在多線程中,因?yàn)槊總€(gè)線程都有自己的工作內(nèi)存(也叫本地內(nèi)存,是線程自己的緩存),變量i
在多個(gè)線程的本地內(nèi)存中都存在一個(gè)副本,如果有兩個(gè)線程執(zhí)行i++
操作:
假設(shè)兩個(gè)線程為A、B,同時(shí)假設(shè)
i
初始值為0線程A從內(nèi)存中讀取
i
的值放入緩存中,此時(shí)i
的值為0,線程B也同理,放入緩存中的值也是0兩個(gè)線程同時(shí)進(jìn)行自增操作,此時(shí)A、B線程的緩存中,
i
的值都是1兩個(gè)線程將
i
寫入主內(nèi)存,相當(dāng)于i
被兩次賦值為1最終結(jié)果是
i
的值為1
這個(gè)就是典型的緩存不一致問(wèn)題,主流的解決辦法有:
總線加鎖
緩存一致性協(xié)議
總線加鎖
這是一種悲觀的實(shí)現(xiàn)方式,具體來(lái)說(shuō),就是通過(guò)處理器發(fā)出lock
指令,鎖住總線,總線收到指令后,會(huì)阻塞其他處理器的請(qǐng)求,直到占用鎖的處理器完成操作。特點(diǎn)是只有一個(gè)搶到總線鎖的處理器運(yùn)行,但是這種方式效率低下,一旦某個(gè)處理器獲取到鎖其他處理器只能阻塞等待,會(huì)影響多核處理器的性能。
緩存一致性協(xié)議
圖示如下:
緩存一致性協(xié)議中最出名的就是MESI
協(xié)議,MESI
保證了每一個(gè)緩存中使用的共享變量的副本都是一致的。大致思想是,CPU
操作緩存中的數(shù)據(jù)時(shí),如果發(fā)現(xiàn)該變量是一個(gè)共享變量,操作如下:
讀?。翰蛔銎渌幚?,只是將緩存中數(shù)據(jù)讀取到寄存器中
寫入:發(fā)出信號(hào)通知其他
CPU
將該變量的緩存行設(shè)置為無(wú)效狀態(tài)(Invalid
),其他CPU
進(jìn)行該變量的讀取時(shí)需要到主存中再次獲取
具體來(lái)說(shuō),MESI
中規(guī)定了緩存行使用4種狀態(tài)標(biāo)記:
M
:Modified
,被修改E
:Exclusive
,獨(dú)享的S
:Shared
,共享的I
:Invalid
,無(wú)效的
有關(guān)MESI
詳細(xì)的實(shí)現(xiàn)超出了本文的范圍,想要詳細(xì)了解可以參考此處或此處。
JMM
看完了CPU
緩存再來(lái)看一下JMM
,也就是Java
內(nèi)存模型,指定了JVM
如何與計(jì)算機(jī)的主存進(jìn)行工作,同時(shí)也決定了一個(gè)線程對(duì)共享變量的寫入何時(shí)對(duì)其他線程可見(jiàn),JMM
定義了線程和主內(nèi)存之間的抽象關(guān)系,具體如下:
共享變量存儲(chǔ)于主內(nèi)存中,每個(gè)線程都可以訪問(wèn)
每個(gè)線程都有私有的工作內(nèi)存或者叫本地內(nèi)存
工作內(nèi)存只存儲(chǔ)該線程對(duì)共享變量的副本
線程不能直接操作主內(nèi)存,只有先操作了工作內(nèi)存之后才能寫入主內(nèi)存
工作內(nèi)存和
JMM
內(nèi)存模型一樣也是一個(gè)抽象概念,其實(shí)并不存在,涵蓋了緩存、寄存器、編譯期優(yōu)化以及硬件等
簡(jiǎn)略圖如下:
與MESI
類似,如果一個(gè)線程修改了共享變量,刷新到主內(nèi)存后,其他線程讀取工作內(nèi)存的時(shí)候發(fā)現(xiàn)緩存失效,會(huì)從主內(nèi)存再次讀取到工作內(nèi)存中。
而下圖表示了JVM
與計(jì)算機(jī)硬件分配的關(guān)系:
并發(fā)編程的三個(gè)特性
文章都看了大半了還沒(méi)到volatile
?別急別急,先來(lái)看看并發(fā)編程中的三個(gè)重要特性,這對(duì)正確理解volatile
有很大的幫助。
原子性
原子性就是在一次或多次操作中:
要么所有的操作全部都得到了執(zhí)行,且不會(huì)受到任何因素的干擾而中斷
要么所有的操作都不執(zhí)行
一個(gè)典型的例子就是兩個(gè)人轉(zhuǎn)賬,比如A向B轉(zhuǎn)賬1000元,那么這包含兩個(gè)基本的操作:
A的賬戶扣除1000元
B的賬戶增加1000元
這兩個(gè)操作,要么都成功,要么都失敗,也就是不能出現(xiàn)A賬戶扣除1000但是B賬戶金額不變的情況,也不能出現(xiàn)A賬戶金額不變B賬戶增加1000的情況。
需要注意的是兩個(gè)原子性操作結(jié)合在一起未必是原子性的,比如i++
。本質(zhì)上來(lái)說(shuō),i++
涉及到了三個(gè)操作:
get i
i+1
set i
這三個(gè)操作都是原子性的,但是組合在一起(i++
)就不是原子性的。
可見(jiàn)性
另一個(gè)重要的特性是可見(jiàn)性,可見(jiàn)性是指,一個(gè)線程對(duì)共享變量進(jìn)行了修改,那么另外的線程可以立即看到修改后的最新值。
一個(gè)簡(jiǎn)單的例子如下:
public class Main { private int x = 0; private static final int MAX = 100000; public static void main(String[] args) throws InterruptedException { Main m = new Main(); Thread thread0 = new Thread(()->{ while(m.x < MAX) { ++m.x; } }); Thread thread1 = new Thread(()->{ while(m.x < MAX){ } System.out.println("finish"); }); thread1.start(); TimeUnit.MILLISECONDS.sleep(1); thread0.start(); } }
線程thread1
會(huì)一直運(yùn)行,因?yàn)?code>thread1把x
讀入工作內(nèi)存后,會(huì)一直判斷工作內(nèi)存中的值,由于thread0
改變的是thread0
工作內(nèi)存的值,并沒(méi)有對(duì)thread1
可見(jiàn),因此永遠(yuǎn)也不會(huì)輸出finish
,使用jstack
也可以看到結(jié)果:
有序性
有序性是指代碼在執(zhí)行過(guò)程中的先后順序,由于JVM
的優(yōu)化,導(dǎo)致了代碼的編寫順序未必是代碼的運(yùn)行順序,比如下面的四條語(yǔ)句:
int x = 10; int y = 0; x++; y = 20;
有可能y=20
在x++
前執(zhí)行,這就是指令重排序。一般來(lái)說(shuō),處理器為了提高程序的效率,可能會(huì)對(duì)輸入的代碼指令做一定的優(yōu)化,不會(huì)嚴(yán)格按照編寫順序去執(zhí)行代碼,但可以保證最終運(yùn)算結(jié)果是編碼時(shí)的期望結(jié)果,當(dāng)然,重排序也有一定的規(guī)則,需要嚴(yán)格遵守指令之間的數(shù)據(jù)依賴關(guān)系,并不是可以任意重排序,比如:
int x = 10; int y = 0; x++; y = x+1;
y=x+1
就不能先優(yōu)于x++
執(zhí)行。
在單線程下重排序不會(huì)導(dǎo)致預(yù)期值的改變,但在多線程下,如果有序性得不到保證,那么將可能出現(xiàn)很大的問(wèn)題:
private boolean initialized = false; private Context context; public Context load(){ if(!initialized){ context = loadContext(); initialized = true; } return context; }
如果發(fā)生了重排序,initialized=true
排序到了context=loadContext()
的前面,假設(shè)兩個(gè)線程A、B同時(shí)訪問(wèn),且loadContext()
需要一定耗時(shí),那么:
線程A通過(guò)判斷后,先設(shè)置布爾變量的值為
true
,再進(jìn)行loadContext()
操作線程B中由于布爾變量被設(shè)置為
true
,會(huì)直接返回一個(gè)未加載完成的context
volatile
好了終于到了volatile
了,前面說(shuō)了這么多,目的就是為了能徹底理解和明白volatile
。這部分分為四個(gè)小節(jié):
volatile
的語(yǔ)義如何保證有序性以及可見(jiàn)性
實(shí)現(xiàn)原理
使用場(chǎng)景
與
synchronized
區(qū)別
先來(lái)介紹一下volatile
的語(yǔ)義。
語(yǔ)義
被volatile
修飾的實(shí)例變量或者類變量具有兩層語(yǔ)義:
保證了不同線程之間對(duì)共享變量操作時(shí)的可見(jiàn)性
禁止對(duì)指令進(jìn)行重排序操作
如何保證可見(jiàn)性以及有序性
先說(shuō)結(jié)論:
volatile
能保證可見(jiàn)性volatile
能保證有序性volatile
不能保證原子性
下面分別進(jìn)行介紹。
可見(jiàn)性
Java
中保證可見(jiàn)性有如下方式:
volatile
:當(dāng)一個(gè)變量被volatile
修飾時(shí),對(duì)共享資源的讀操作會(huì)直接在主內(nèi)存中進(jìn)行(準(zhǔn)確來(lái)說(shuō)也會(huì)讀取到工作內(nèi)存中,但是如果其他線程進(jìn)行了修改就必須從主內(nèi)存重新讀?。瑢懖僮魇窍刃薷墓ぷ鲀?nèi)存,但是修改結(jié)束后立即刷新到主內(nèi)存中synchronized
:synchronized
一樣能保證可見(jiàn)性,能夠保證同一時(shí)刻只有一個(gè)線程獲取到鎖,然后執(zhí)行同步方法,并且確保鎖釋放之前,變量的修改被刷新到主內(nèi)存中使用顯式鎖
Lock
:Lock
的lock
方法能保證同一時(shí)刻只有一個(gè)線程能夠獲取到鎖然后執(zhí)行同步方法,并且確保鎖釋放之前能夠?qū)?duì)變量的修改刷新到主內(nèi)存中
具體來(lái)說(shuō),可以看一下之前的例子:
public class Main { private int x = 0; private static final int MAX = 100000; public static void main(String[] args) throws InterruptedException { Main m = new Main(); Thread thread0 = new Thread(()->{ while(m.x < MAX) { ++m.x; } }); Thread thread1 = new Thread(()->{ while(m.x < MAX){ } System.out.println("finish"); }); thread1.start(); TimeUnit.MILLISECONDS.sleep(1); thread0.start(); } }
上面說(shuō)過(guò)這段代碼會(huì)不斷運(yùn)行,一直沒(méi)有輸出,就是因?yàn)樾薷暮蟮?code>x對(duì)線程thread1
不可見(jiàn),如果在x
的定義中加上了volatile
,就不會(huì)出現(xiàn)沒(méi)有輸出的情況了,因?yàn)榇藭r(shí)對(duì)x
的修改是線程thread1
可見(jiàn)的。有序性
JMM
中允許編譯期和處理器對(duì)指令進(jìn)行重排序,在多線程的情況下有可能會(huì)出現(xiàn)問(wèn)題,為此,Java
同樣提供了三種機(jī)制去保證有序性:
volatile
synchronized
顯式鎖
Lock
另外,關(guān)于有序性不得不提的就是Happens-before
原則。Happends-before
原則說(shuō)的就是如果兩個(gè)操作的執(zhí)行次序無(wú)法從該原則推導(dǎo)出來(lái),那么就無(wú)法保證有序性,JVM
或處理器可以任意重排序。這么做的目的是為了盡可能提高程序的并行度,具體規(guī)則如下:
程序次序規(guī)則:在一個(gè)線程內(nèi),代碼按照編寫時(shí)的次序執(zhí)行,編寫在后面的操作發(fā)生與編寫在前面的操作之后
鎖定規(guī)則:如果一個(gè)鎖處于鎖定狀態(tài),則
unlock
操作要先行發(fā)生于對(duì)同一個(gè)鎖的lock
操作volatile
變量規(guī)則:對(duì)一個(gè)變量的寫操作要早于對(duì)這個(gè)變量之后的讀操作傳遞規(guī)則:如果操作A先于操作B,操作B先于操作C,那么操作A先于操作C
線程啟動(dòng)規(guī)則:
Thread
對(duì)象的start()
方法先行發(fā)生于對(duì)該線程的任何動(dòng)作線程中斷規(guī)則:對(duì)線程執(zhí)行
interrupt()
方法肯定要優(yōu)于捕獲到中斷信號(hào),換句話說(shuō),如果收到了中斷信號(hào),那么在此之前必定調(diào)用了interrupt()
線程終結(jié)規(guī)則:線程中所有操作都要先行發(fā)生于線程的終止檢測(cè),也就是邏輯單元的執(zhí)行肯定要發(fā)生于線程終止之前
對(duì)象終結(jié)規(guī)則:一個(gè)對(duì)象初始化的完成先行發(fā)生于
finalize()
之前
對(duì)于volatile
,會(huì)直接禁止對(duì)指令重排,但是對(duì)于volatile
前后無(wú)依賴關(guān)系的指令可以隨意重排,比如:
int x = 0; int y = 1; //private volatile int z; z = 20; x++; y--;
在z=20
之前,先定義x
或先定義y
并沒(méi)有要求,只需要在執(zhí)行z=20
的時(shí)候,可以保證x=0,y=1
即可,同理,x++
或y--
具體先執(zhí)行哪一個(gè)并沒(méi)有要求,只需要保證兩者執(zhí)行在z=20
之后即可。
原子性
在Java
中,所有對(duì)基本數(shù)據(jù)類型變量的讀取賦值操作都是原子性的,對(duì)引用類型的變量讀取和賦值也是原子性的,但是:
將一個(gè)變量賦值給另一個(gè)變量的操作不是原子性的,因?yàn)樯婕暗搅艘粋€(gè)變量的讀取以及一個(gè)變量的寫入,兩個(gè)原子性操作結(jié)合在一起就不是原子性操作
多個(gè)原子性操作在一起就不是原子性操作,比如
i++
JMM
只保證基本讀取和賦值的原子性操作,其他的均不保證,如果需要具備原子性,那么可以使用synchronized
或Lock
,或者JUC
包下的原子操作類
也就是說(shuō),volatile
并不能保證原子性,例子如下:
public class Main { private volatile int x = 0; private static final CountDownLatch latch = new CountDownLatch(10); public void inc() { ++x; } public static void main(String[] args) throws InterruptedException { Main m = new Main(); IntStream.range(0, 10).forEach(i -> { new Thread(() -> { for (int j = 0; j < 1000; j++) { m.inc(); } latch.countDown(); }).start(); }); latch.await(); System.out.println(m.x); } }
最后輸出的x
的值會(huì)少于10000
,而且每次運(yùn)行的結(jié)果也并不相同,至于原因,可以從兩個(gè)線程A、B開(kāi)始分析,圖示如下:
0-t1
:線程A將x
讀入工作內(nèi)存,此時(shí)x=0
t1-t2
:線程A時(shí)間片完,CPU
調(diào)度線程B,線程B將x
讀入工作內(nèi)存,此時(shí)x=0
t2-t3
:線程B對(duì)工作內(nèi)存中的x
進(jìn)行自增操作,并更新到工作內(nèi)存中t3-t4
:線程B時(shí)間片完,CPU
調(diào)度線程A,同理線程A對(duì)工作內(nèi)存中的x
自增t4-t5
:線程A將工作內(nèi)存中的值寫回主內(nèi)存,此時(shí)主內(nèi)存中的值為x=1
t5
以后:線程A時(shí)間片完,CPU
調(diào)度線程B,線程B也將自己的工作內(nèi)存寫回主內(nèi)存,再次將主內(nèi)存中的x
賦值為1
也就是說(shuō),多線程操作的話,會(huì)出現(xiàn)兩次自增但是實(shí)際上只進(jìn)行一次數(shù)值修改的操作。想要x
的值變?yōu)?code>10000也很簡(jiǎn)單,加上synchronized
即可:
new Thread(() -> { synchronized (m) { for (int j = 0; j < 1000; j++) { m.inc(); } } latch.countDown(); }).start();
實(shí)現(xiàn)原理
前面已經(jīng)知道,volatile
可以保證有序性以及可見(jiàn)性,那么,具體是如何操作的呢?
答案就是一個(gè)lock;
前綴,該前綴實(shí)際上相當(dāng)于一個(gè)內(nèi)存屏障,該內(nèi)存屏障會(huì)為指令的執(zhí)行提供如下幾個(gè)保障:
確保指令重排序時(shí)不會(huì)將其后面的代碼排到內(nèi)存屏障之前
確保指令重排序時(shí)不會(huì)將其前面的代碼排到內(nèi)存屏障之后
確保執(zhí)行到內(nèi)存屏障修飾的指令時(shí)前面的代碼全部執(zhí)行完成
強(qiáng)制將線程工作內(nèi)存中的值修改刷新到主存中
如果是寫操作,會(huì)導(dǎo)致其他線程工作內(nèi)存中的緩存數(shù)據(jù)失效
使用場(chǎng)景
一個(gè)典型的使用場(chǎng)景是利用開(kāi)關(guān)進(jìn)行線程的關(guān)閉操作,例子如下:
public class ThreadTest extends Thread{ private volatile boolean started = true; @Override public void run() { while (started){ } } public void shutdown(){ this.started = false; } }
如果布爾變量沒(méi)有被volatile
修飾,那么很可能新的布爾值刷新不到主內(nèi)存中,導(dǎo)致線程不會(huì)結(jié)束。
與synchronized
的區(qū)別
使用上的區(qū)別:
volatile
只能用于修飾實(shí)例變量或者類變量,但是不能用于修飾方法、方法參數(shù)、局部變量等,另外可以修飾的變量為null
。但synchronized
不能用于對(duì)變量的修飾,只能修飾方法或語(yǔ)句塊,而且monitor
對(duì)象不能為null
對(duì)原子性的保證:
volatile
無(wú)法保證原子性,但是synchronized
可以保證對(duì)可見(jiàn)性的保證:
volatile
與synchronized
都能保證可見(jiàn)性,但是synchronized
是借助于JVM
指令monitor enter
/monitor exit
保證的,在monitor exit
的時(shí)候所有共享資源都被刷新到主內(nèi)存中,而volatile
是通過(guò)lock;
機(jī)器指令實(shí)現(xiàn)的,迫使其他線程工作內(nèi)存失效,需要到主內(nèi)存加載對(duì)有序性的保證:
volatile
能夠禁止JVM
以及處理器對(duì)其進(jìn)行重排序,而synchronized
保證的有序性是通過(guò)程序串行化執(zhí)行換來(lái)的,并且在synchronized
代碼塊中的代碼也會(huì)發(fā)生指令重排的情況其他區(qū)別:
volatile
不會(huì)使線程陷入阻塞,但synchronized
會(huì)
到此,關(guān)于“如何理解Java中volatile關(guān)鍵字”的學(xué)習(xí)就結(jié)束了,希望能夠解決大家的疑惑。理論與實(shí)踐的搭配能更好的幫助大家學(xué)習(xí),快去試試吧!若想繼續(xù)學(xué)習(xí)更多相關(guān)知識(shí),請(qǐng)繼續(xù)關(guān)注創(chuàng)新互聯(lián)網(wǎng)站,小編會(huì)繼續(xù)努力為大家?guī)?lái)更多實(shí)用的文章!
文章名稱:如何理解Java中volatile關(guān)鍵字
當(dāng)前鏈接:http://www.dlmjj.cn/article/ihgsip.html