內存可見性java
留意複合類操做編程
禁止指令重排序多線程
總結併發
volatile是Java提供的一種輕量級的同步機制,在併發編程中,它也扮演着比較重要的角色。同synchronized相比(synchronized一般稱爲重量級鎖),volatile更輕量級,相比使用synchronized所帶來的龐大開銷,假若能恰當的合理的使用volatile,天然是美事一樁。性能
爲了能比較清晰完全的理解volatile,咱們一步一步來分析。首先來看看以下代碼優化
public class TestVolatile { boolean status = false; /** * 狀態切換爲true */ public void changeStatus(){ status = true; } /** * 若狀態爲true,則running。 */ public void run(){ if(status){ System.out.println("running...."); } } }
上面這個例子,在多線程環境裏,假設線程A執行changeStatus()方法後,線程B運行run()方法,能夠保證輸出"running....."嗎?spa
答案是NO! 操作系統
這個結論會讓人有些疑惑,能夠理解。由於假若在單線程模型裏,先運行changeStatus方法,再執行run方法,天然是能夠正確輸出"running...."的;可是在多線程模型中,是無法作這種保證的。由於對於共享變量status來講,線程A的修改,對於線程B來說,是"不可見"的。也就是說,線程B此時可能沒法觀測到status已被修改成true。那麼什麼是可見性呢?線程
所謂可見性,是指當一條線程修改了共享變量的值,新值對於其餘線程來講是能夠當即得知的。很顯然,上述的例子中是沒有辦法作到內存可見性的。
Java內存模型
爲何出現這種狀況呢,咱們須要先了解一下JMM(java內存模型)
java虛擬機有本身的內存模型(Java Memory Model,JMM),JMM能夠屏蔽掉各類硬件和操做系統的內存訪問差別,以實現讓java程序在各類平臺下都能達到一致的內存訪問效果。
JMM決定一個線程對共享變量的寫入什麼時候對另外一個線程可見,JMM定義了線程和主內存之間的抽象關係:共享變量存儲在主內存(Main Memory)中,每一個線程都有一個私有的本地內存(Local Memory),本地內存保存了被該線程使用到的主內存的副本拷貝,線程對變量的全部操做都必須在工做內存中進行,而不能直接讀寫主內存中的變量。這三者之間的交互關係以下
須要注意的是,JMM是個抽象的內存模型,因此所謂的本地內存,主內存都是抽象概念,並不必定就真實的對應cpu緩存和物理內存。固然若是是出於理解的目的,這樣對應起來也無不可。
大概瞭解了JMM的簡單定義後,問題就很容易理解了,對於普通的共享變量來說,好比咱們上文中的status,線程A將其修改成true這個動做發生在線程A的本地內存中,此時還未同步到主內存中去;而線程B緩存了status的初始值false,此時可能沒有觀測到status的值被修改了,因此就致使了上述的問題。那麼這種共享變量在多線程模型中的不可見性如何解決呢?比較粗暴的方式天然就是加鎖,可是此處使用synchronized或者Lock這些方式過重量級了,有點炮打蚊子的意思。比較合理的方式其實就是volatile
volatile具有兩種特性,第一就是保證共享變量對全部線程的可見性。將一個共享變量聲明爲volatile後,會有如下效應:
1.當寫一個volatile變量時,JMM會把該線程對應的本地內存中的變量強制刷新到主內存中去;
2.這個寫會操做會致使其餘線程中的緩存無效。
上面的例子只需將status聲明爲volatile,便可保證在線程A將其修改成true時,線程B能夠馬上得知
volatile boolean status = false;
可是須要注意的是,咱們一直在拿volatile和synchronized作對比,僅僅是由於這兩個關鍵字在某些內存語義上有共通之處,volatile並不能徹底替代synchronized,它依然是個輕量級鎖,在不少場景下,volatile並不能勝任。看下這個例子:
package test; import java.util.concurrent.CountDownLatch; /** * Created by chengxiao on 2017/3/18. */ public class Counter { public static volatile int num = 0; //使用CountDownLatch來等待計算線程執行完 static CountDownLatch countDownLatch = new CountDownLatch(30); public static void main(String []args) throws InterruptedException { //開啓30個線程進行累加操做 for(int i=0;i<30;i++){ new Thread(){ public void run(){ for(int j=0;j<10000;j++){ num++;//自加操做 } countDownLatch.countDown(); } }.start(); } //等待計算線程執行完 countDownLatch.await(); System.out.println(num); } }
執行結果:
224291
針對這個示例,一些同窗可能會以爲疑惑,若是用volatile修飾的共享變量能夠保證可見性,那麼結果不該該是300000麼?
問題就出在num++這個操做上,由於num++不是個原子性的操做,而是個複合操做。咱們能夠簡單講這個操做理解爲由這三步組成:
1.讀取
2.加一
3.賦值
因此,在多線程環境下,有可能線程A將num讀取到本地內存中,此時其餘線程可能已經將num增大了不少,線程A依然對過時的num進行自加,從新寫到主存中,最終致使了num的結果不合預期,而是小於30000。
針對num++這類複合類的操做,可使用java併發包中的原子操做類原子操做類是經過循環CAS的方式來保證其原子性的。
/** * Created by chengxiao on 2017/3/18. */ public class Counter {
//使用原子操做類 public static AtomicInteger num = new AtomicInteger(0); //使用CountDownLatch來等待計算線程執行完 static CountDownLatch countDownLatch = new CountDownLatch(30); public static void main(String []args) throws InterruptedException { //開啓30個線程進行累加操做 for(int i=0;i<30;i++){ new Thread(){ public void run(){ for(int j=0;j<10000;j++){ num.incrementAndGet();//原子性的num++,經過循環CAS方式 } countDownLatch.countDown(); } }.start(); } //等待計算線程執行完 countDownLatch.await(); System.out.println(num); } }
執行結果
300000
關於原子類操做的基本原理,會在後面的章節進行介紹,此處再也不贅述。
volatile還有一個特性:禁止指令重排序優化。
重排序是指編譯器和處理器爲了優化程序性能而對指令序列進行排序的一種手段。可是重排序也須要遵照必定規則:
1.重排序操做不會對存在數據依賴關係的操做進行重排序。
好比:a=1;b=a; 這個指令序列,因爲第二個操做依賴於第一個操做,因此在編譯時和處理器運行時這兩個操做不會被重排序。
2.重排序是爲了優化性能,可是無論怎麼重排序,單線程下程序的執行結果不能被改變
好比:a=1;b=2;c=a+b這三個操做,第一步(a=1)和第二步(b=2)因爲不存在數據依賴關係,因此可能會發生重排序,可是c=a+b這個操做是不會被重排序的,由於須要保證最終的結果必定是c=a+b=3。
重排序在單線程模式下是必定會保證最終結果的正確性,可是在多線程環境下,問題就出來了,來開個例子,咱們對第一個TestVolatile的例子稍稍改進,再增長個共享變量a
public class TestVolatile { int a = 1; boolean status = false; /** * 狀態切換爲true */ public void changeStatus(){ a = 2;//1 status = true;//2 } /** * 若狀態爲true,則running。 */ public void run(){ if(status){//3 int b = a+1;//4 System.out.println(b); } } }
假設線程A執行changeStatus後,線程B執行run,咱們能保證在4處,b必定等於3麼?
答案依然是沒法保證!也有可能b仍然爲2。上面咱們提到過,爲了提供程序並行度,編譯器和處理器可能會對指令進行重排序,而上例中的1和2因爲不存在數據依賴關係,則有可能會被重排序,先執行status=true再執行a=2。而此時線程B會順利到達4處,而線程A中a=2這個操做還未被執行,因此b=a+1的結果也有可能依然等於2。
使用volatile關鍵字修飾共享變量即可以禁止這種重排序。若用volatile修飾共享變量,在編譯時,會在指令序列中插入內存屏障來禁止特定類型的處理器重排序
volatile禁止指令重排序也有一些規則,簡單列舉一下:
1.當第二個操做是voaltile寫時,不管第一個操做是什麼,都不能進行重排序
2.當地一個操做是volatile讀時,無論第二個操做是什麼,都不能進行重排序
3.當第一個操做是volatile寫時,第二個操做是volatile讀時,不能進行重排序
簡單總結下,volatile是一種輕量級的同步機制,它主要有兩個特性:一是保證共享變量對全部線程的可見性;二是禁止指令重排序優化。同時須要注意的是,volatile對於單個的共享變量的讀/寫具備原子性,可是像num++這種複合操做,volatile沒法保證其原子性,固然文中也提出瞭解決方案,就是使用併發包中的原子操做類,經過循環CAS地方式來保證num++操做的原子性。關於原子操做類,會在後續的文章進行介紹。