您好,登錄后才能下訂單哦!
今天就跟大家聊聊有關(guān)一文讀懂Java中的同步機(jī)制volatile,可能很多人都不太了解,為了讓大家更加了解,小編給大家總結(jié)了以下內(nèi)容,希望大家根據(jù)這篇文章可以有所收獲。
內(nèi)存可見性
volatile是Java提供的一種輕量級的同步機(jī)制,在并發(fā)編程中,它也扮演著比較重要的角色。同synchronized相比(synchronized通常稱為重量級鎖),volatile更輕量級,相比使用synchronized時(shí)引起的線程上下文切換所帶來的龐大開銷,倘若能恰當(dāng)?shù)暮侠淼氖褂胿olatile,自然是美事一樁。
為了能比較清晰徹底的理解volatile,我們一步一步來分析。首先來看看如下代碼
public class TestVolatile { boolean status = false; /** * 狀態(tài)切換為true */ public void changeStatus(){ status = true; } /** * 若狀態(tài)為true,則running。 */ public void run(){ if(status){ System.out.println("running...."); } } }
上面這個(gè)例子,在多線程環(huán)境里,假設(shè)線程A執(zhí)行changeStatus()方法后,線程B運(yùn)行run()方法,可以保證輸出"running....."嗎?
答案是NO!
這個(gè)結(jié)論會讓人有些疑惑,可以理解。因?yàn)樘热粼趩尉€程模型里,先運(yùn)行changeStatus方法,再執(zhí)行run方法,自然是可以正確輸出"running...."的;但是在多線程模型中,是沒法做這種保證的。因?yàn)閷τ诠蚕碜兞縮tatus來說,線程A的修改,對于線程B來講,是"不可見"的。也就是說,線程B此時(shí)可能無法觀測到status已被修改為true。那么什么是可見性呢?
所謂可見性,是指當(dāng)一條線程修改了共享變量的值,新值對于其他線程來說是可以立即得知的。很顯然,上述的例子中是沒有辦法做到內(nèi)存可見性的。
Java內(nèi)存模型
為什么出現(xiàn)這種情況呢,我們需要先了解一下JMM(java內(nèi)存模型)
java虛擬機(jī)有自己的內(nèi)存模型(Java Memory Model,JMM),JMM可以屏蔽掉各種硬件和操作系統(tǒng)的內(nèi)存訪問差異,以實(shí)現(xiàn)讓java程序在各種平臺下都能達(dá)到一致的內(nèi)存訪問效果。
JMM決定一個(gè)線程對共享變量的寫入何時(shí)對另一個(gè)線程可見,JMM定義了線程和主內(nèi)存之間的抽象關(guān)系:共享變量存儲在主內(nèi)存(Main Memory)中,每個(gè)線程都有一個(gè)私有的本地內(nèi)存(Local Memory),本地內(nèi)存保存了被該線程使用到的主內(nèi)存的副本拷貝,線程對變量的所有操作都必須在工作內(nèi)存中進(jìn)行,而不能直接讀寫主內(nèi)存中的變量。這三者之間的交互關(guān)系如下
需要注意的是,JMM是個(gè)抽象的內(nèi)存模型,所以所謂的本地內(nèi)存,主內(nèi)存都是抽象概念,并不一定就真實(shí)的對應(yīng)cpu緩存和物理內(nèi)存。當(dāng)然如果是出于理解的目的,這樣對應(yīng)起來也無不可。
大概了解了JMM的簡單定義后,問題就很容易理解了,對于普通的共享變量來講,比如我們上文中的status,線程A將其修改為true這個(gè)動作發(fā)生在線程A的本地內(nèi)存中,此時(shí)還未同步到主內(nèi)存中去;而線程B緩存了status的初始值false,此時(shí)可能沒有觀測到status的值被修改了,所以就導(dǎo)致了上述的問題。那么這種共享變量在多線程模型中的不可見性如何解決呢?比較粗暴的方式自然就是加鎖,但是此處使用synchronized或者Lock這些方式太重量級了,有點(diǎn)炮打蚊子的意思。比較合理的方式其實(shí)就是volatile
volatile具備兩種特性,第一就是保證共享變量對所有線程的可見性。將一個(gè)共享變量聲明為volatile后,會有以下效應(yīng):
1.當(dāng)寫一個(gè)volatile變量時(shí),JMM會把該線程對應(yīng)的本地內(nèi)存中的變量強(qiáng)制刷新到主內(nèi)存中去;
2.這個(gè)寫會操作會導(dǎo)致其他線程中的緩存無效。
上面的例子只需將status聲明為volatile,即可保證在線程A將其修改為true時(shí),線程B可以立刻得知
volatile boolean status = false;
留意復(fù)合類操作
但是需要注意的是,我們一直在拿volatile和synchronized做對比,僅僅是因?yàn)檫@兩個(gè)關(guān)鍵字在某些內(nèi)存語義上有共通之處,volatile并不能完全替代synchronized,它依然是個(gè)輕量級鎖,在很多場景下,volatile并不能勝任??聪逻@個(gè)例子:
package test; import java.util.concurrent.CountDownLatch; /** * Created by chengxiao on 2017/3/18. */ public class Counter { public static volatile int num = 0; //使用CountDownLatch來等待計(jì)算線程執(zhí)行完 static CountDownLatch countDownLatch = new CountDownLatch(30); public static void main(String []args) throws InterruptedException { //開啟30個(gè)線程進(jìn)行累加操作 for(int i=0;i<30;i++){ new Thread(){ public void run(){ for(int j=0;j<10000;j++){ num++;//自加操作 } countDownLatch.countDown(); } }.start(); } //等待計(jì)算線程執(zhí)行完 countDownLatch.await(); System.out.println(num); } }
執(zhí)行結(jié)果:
224291
針對這個(gè)示例,一些同學(xué)可能會覺得疑惑,如果用volatile修飾的共享變量可以保證可見性,那么結(jié)果不應(yīng)該是300000么?
問題就出在num++這個(gè)操作上,因?yàn)閚um++不是個(gè)原子性的操作,而是個(gè)復(fù)合操作。我們可以簡單講這個(gè)操作理解為由這三步組成:
1.讀取
2.加一
3.賦值
所以,在多線程環(huán)境下,有可能線程A將num讀取到本地內(nèi)存中,此時(shí)其他線程可能已經(jīng)將num增大了很多,線程A依然對過期的num進(jìn)行自加,重新寫到主存中,最終導(dǎo)致了num的結(jié)果不合預(yù)期,而是小于30000。
解決num++操作的原子性問題
針對num++這類復(fù)合類的操作,可以使用java并發(fā)包中的原子操作類原子操作類是通過循環(huán)CAS的方式來保證其原子性的。
/** * Created by chengxiao on 2017/3/18. */ public class Counter { //使用原子操作類 public static AtomicInteger num = new AtomicInteger(0); //使用CountDownLatch來等待計(jì)算線程執(zhí)行完 static CountDownLatch countDownLatch = new CountDownLatch(30); public static void main(String []args) throws InterruptedException { //開啟30個(gè)線程進(jìn)行累加操作 for(int i=0;i<30;i++){ new Thread(){ public void run(){ for(int j=0;j<10000;j++){ num.incrementAndGet();//原子性的num++,通過循環(huán)CAS方式 } countDownLatch.countDown(); } }.start(); } //等待計(jì)算線程執(zhí)行完 countDownLatch.await(); System.out.println(num); } }
執(zhí)行結(jié)果
300000
關(guān)于原子類操作的基本原理,會在后面的章節(jié)進(jìn)行介紹,此處不再贅述。
禁止指令重排序
volatile還有一個(gè)特性:禁止指令重排序優(yōu)化。
重排序是指編譯器和處理器為了優(yōu)化程序性能而對指令序列進(jìn)行排序的一種手段。但是重排序也需要遵守一定規(guī)則:
1.重排序操作不會對存在數(shù)據(jù)依賴關(guān)系的操作進(jìn)行重排序。
比如:a=1;b=a; 這個(gè)指令序列,由于第二個(gè)操作依賴于第一個(gè)操作,所以在編譯時(shí)和處理器運(yùn)行時(shí)這兩個(gè)操作不會被重排序。
2.重排序是為了優(yōu)化性能,但是不管怎么重排序,單線程下程序的執(zhí)行結(jié)果不能被改變
比如:a=1;b=2;c=a+b這三個(gè)操作,第一步(a=1)和第二步(b=2)由于不存在數(shù)據(jù)依賴關(guān)系,所以可能會發(fā)生重排序,但是c=a+b這個(gè)操作是不會被重排序的,因?yàn)樾枰WC最終的結(jié)果一定是c=a+b=3。
重排序在單線程模式下是一定會保證最終結(jié)果的正確性,但是在多線程環(huán)境下,問題就出來了,來開個(gè)例子,我們對第一個(gè)TestVolatile的例子稍稍改進(jìn),再增加個(gè)共享變量a
public class TestVolatile { int a = 1; boolean status = false; /** * 狀態(tài)切換為true */ public void changeStatus(){ a = 2;//1 status = true;//2 } /** * 若狀態(tài)為true,則running。 */ public void run(){ if(status){//3 int b = a+1;//4 System.out.println(b); } } }
假設(shè)線程A執(zhí)行changeStatus后,線程B執(zhí)行run,我們能保證在4處,b一定等于3么?
答案依然是無法保證!也有可能b仍然為2。上面我們提到過,為了提供程序并行度,編譯器和處理器可能會對指令進(jìn)行重排序,而上例中的1和2由于不存在數(shù)據(jù)依賴關(guān)系,則有可能會被重排序,先執(zhí)行status=true再執(zhí)行a=2。而此時(shí)線程B會順利到達(dá)4處,而線程A中a=2這個(gè)操作還未被執(zhí)行,所以b=a+1的結(jié)果也有可能依然等于2。
使用volatile關(guān)鍵字修飾共享變量便可以禁止這種重排序。若用volatile修飾共享變量,在編譯時(shí),會在指令序列中插入內(nèi)存屏障來禁止特定類型的處理器重排序
volatile禁止指令重排序也有一些規(guī)則,簡單列舉一下:
1.當(dāng)?shù)诙€(gè)操作是voaltile寫時(shí),無論第一個(gè)操作是什么,都不能進(jìn)行重排序
2.當(dāng)?shù)匾粋€(gè)操作是volatile讀時(shí),不管第二個(gè)操作是什么,都不能進(jìn)行重排序
3.當(dāng)?shù)谝粋€(gè)操作是volatile寫時(shí),第二個(gè)操作是volatile讀時(shí),不能進(jìn)行重排序
看完上述內(nèi)容,你們對一文讀懂Java中的同步機(jī)制volatile有進(jìn)一步的了解嗎?如果還想了解更多知識或者相關(guān)內(nèi)容,請關(guān)注億速云行業(yè)資訊頻道,感謝大家的支持。
免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。