本博客系列是學習併發編程過程當中的記錄總結。因爲文章比較多,寫的時間也比較散,因此我整理了個目錄貼(傳送門),方便查閱。html
併發編程系列博客傳送門java
以前的文章中講到,JMM是內存模型規範在Java語言中的體現。JMM保證了在多核CPU多線程編程環境下,對共享變量讀寫的原子性、可見性和有序性。編程
本文就具體來說講JMM是如何保證共享變量訪問的可見性的。多線程
咱們從一段簡單的代碼來看看到底什麼是可見性問題。併發
public class VolatileDemo { boolean started = false; public void startSystem(){ System.out.println(Thread.currentThread().getName()+" begin to start system, time:"+System.currentTimeMillis()); started = true; System.out.println(Thread.currentThread().getName()+" success to start system, time:"+System.currentTimeMillis()); } public void checkStartes(){ if (started){ System.out.println("system is running, time:"+System.currentTimeMillis()); }else { System.out.println("system is not running, time:"+System.currentTimeMillis()); } } public static void main(String[] args) { VolatileDemo demo = new VolatileDemo(); Thread startThread = new Thread(new Runnable() { @Override public void run() { demo.startSystem(); } }); startThread.setName("start-Thread"); Thread checkThread = new Thread(new Runnable() { @Override public void run() { while (true){ demo.checkStartes(); } } }); checkThread.setName("check-Thread"); startThread.start(); checkThread.start(); } }
上面的列子中,一個線程來改變started
的狀態,另一個線程不停地來檢測started
的狀態,若是是true就輸出系統啓動,若是是false就輸出系統未啓動。那麼當start-Thread
線程將狀態改爲true後,check-Thread
線程在執行時是否能當即「看到」這個變化呢?答案是不必定能當即看到。這邊我作了不少測試,大多數狀況下是能「感知」到started這個變量的變化的。可是偶爾會存在感知不到的狀況。請看下下面日誌記錄:ide
start-Thread begin to start system, time:1577079553515 start-Thread success to start system, time:1577079553516 system is not running, time:1577079553516 ==>此處start-Thread線程已經將狀態設置成true,可是check-Thread線程仍是沒檢測到 system is running, time:1577079553516 system is running, time:1577079553516 system is running, time:1577079553516 system is running, time:1577079553516 system is running, time:1577079553516 system is running, time:1577079553516 system is running, time:1577079553517 system is running, time:1577079553517 system is running, time:1577079553517 system is running, time:1577079553517 system is running, time:1577079553517 system is running, time:1577079553517 system is running, time:1577079553517 system is running, time:1577079553519 system is running, time:1577079553519 system is running, time:1577079553519 system is running, time:1577079553519 system is running, time:1577079553519 system is running, time:1577079553519 system is running, time:1577079553519 system is running, time:1577079553519 system is running, time:1577079553519
上面的現象可能會讓人比較困惑,爲何有時候check-Thread
線程能感知到狀態的變化,有時候又感知不到變化呢?這個現象就是在多核CPU多線程編程環境下會出現的可見性問題。性能
Java內存模型規定了全部的變量都存儲在主內存中,每條線程還有本身的工做內存,線程在工做內存中保存的值是主內存中值的副本,線程對變量的全部操做都必須在工做內存中進行,而不能直接讀寫主內存。等到線程對變量操做完畢以後會將變量的最新值刷新回到主內存。學習
可是什麼時候刷新這個最新值又是隨機的。因此就有可能一個線程已經將一個共享變量更新了,可是還沒刷新回主內存,那麼這時其餘對這個變量進行讀寫的線程就看不到這個最新值。這個就是多CPU多線程編程環境下的可見性問題。也是上面代碼會出現問題的緣由。測試
在多CPU多線程編程環境下,對共享變量的讀寫會出現可見性問題。可是幸虧JMM提供了相應的技術手段來幫咱們規避這些問題,可讓程序正確運行。JMM針對可見性問題,主要提供了以下手段:atom
使用volatile關鍵字修飾一個變量能夠保證變量的可見性。因此對於上面的代碼,咱們只須要簡單的修改下代碼就可讓程序正確運行了。
private volatile boolean started = false;
使用volatile修飾一個共享變量能夠達到以下的效果:
那麼volatile具體是怎麼達到上面兩個效果的呢?其實volatile底層使用的是內存屏障來保證可見性的。
內存屏障(英語:Memory barrier),也稱內存柵欄,內存柵障,屏障指令等,是一類同步屏障指令,是CPU或編譯器在對內存隨機訪問的操做中的一個同步點,使得此點以前的全部讀寫操做都執行後才能夠開始執行此點以後的操做。大多數現代計算機爲了提升性能而採起亂序執行,這使得內存屏障成爲必須。
語義上,內存屏障以前的全部寫操做都要寫入內存;內存屏障以後的讀操做均可以得到同步屏障以前的寫操做的結果。所以,對於敏感的程序塊,寫操做以後、讀操做以前能夠插入內存屏障。
對內存屏障作下簡單的總結:
使用synchronized代碼塊或者synchronized方法也能夠保證共享變量的可見性。只要以下修改上面的代碼,咱們就能獲得正確的執行結果。
public synchronized void startSystem(){ System.out.println(Thread.currentThread().getName()+" begin to start system, time:"+System.currentTimeMillis()); value = 2; started = true; System.out.println(Thread.currentThread().getName()+" success to start system, time:"+System.currentTimeMillis()); } public synchronized void checkStartes(){ if (started){ System.out.println("system is running, time:"+System.currentTimeMillis()); }else { System.out.println("system is not running, time:"+System.currentTimeMillis()); } }
當線程釋放鎖時,JMM會把該線程對應的本地內存中的共享變量刷新到主內存中。當線程獲取鎖時,JMM會把該線程對應的本地內存置爲無效。從而使得被監視器保護的臨界區代碼必須從主內存中讀取共享變量。咱們發現鎖具備和volatile一致的內存語義,因此使用synchronized也能夠實現共享變量的可見性。
使用Lock相關的實現類也能夠保證共享變量的可見性。其實現原理和synchronized的實現原理相似,這邊也就再也不贅述了。
使用原子操做類也能夠保證共享變量操做的可見性。因此咱們只要以下修稿上面的代碼就好了。
private AtomicBoolean started = new AtomicBoolean(false);
原子操做類底層使用的是CAS機制。Java中CAS機制每次都會從主內存中獲取最新值進行compare,比較一致以後纔會將新值set到主內存中去。並且這個整個操做是一個原子操做。因此CAS操做每次拿到的都是主內存中的最新值,每次set的值也會當即寫到主內存中。