1. 程式人生 > >幹貨:Java並發編程系列之volatile(一)

幹貨:Java並發編程系列之volatile(一)

並發編程 程序員 Java 架構 volatil

Java語言規範第三版中對volatile的定義如下:Java編程語言允許線程訪問共享變量,為了確保共享變量能被準確和一致地更新,線程應該確保通過排他鎖單獨獲得這個變量。

了解volatile關鍵字之前需要先了解下Java內存模型,java內存模型抽象示意圖如下:

Java內存模型

技術分享圖片

線程A和線程B之間若要通信的話, 必須經歷下面兩個步驟 (1)線程A和線程A本地內存中更新過的共享變量刷新到主存中去。 (2)線程B到主存中去讀取線程A之前更新過的共享變量。

由此可見執行下面的語句:

int a = 100 線程必須現在自己的工作線程中對變量i所在的緩存進行賦值操作,然後再寫入主存當中,而不是直接將數值100寫入主存中。

特性

可見性 當一個共享變量被volatile修飾時,它會保證修改的值立即被更新到主存,所以對其他線程是可見的。當其他線程需要讀取該值時,其他線程會去主存中讀取新值。相反普通的共享變量不能保證可見性,因為普通共享變量被修改後並不會立即被寫入主存,何時被寫入主存也不確定。當其他線程去讀取該值時,此時主存可能還是原來的舊值,這樣就無法保證可見性。

有序性 java內存模型中允許編譯器和處理器對指令進行重排序,雖然重排序過程不會影響到單線程 執行的正確性,但是會影響到多線程並發執行的正確性。這時可以通過volatile來保證有序性,除了volatile,也可以通過synchronized和Lock來保證有序性。synchronized和Lock保證每個時刻只有一個線程執行同步代碼,這相當於讓線程順序執行同步代碼,從而保證了有序性。如果不考慮原子性操作的話volatile比synchronized和Lock更輕量級,成本更低。

不保障原子性 volatile關鍵字只能保證共享變量的可見性和有序性。如果volatile修飾並發線程中共享變量, 而該共享變量是非原子操作的話,並發中就會出現問題。比如下面代碼:

publicclassHelloVolatile{publicvolatileintmNumber =0;publicstaticvoidmain(String []args){ final HelloVolatile hello =newHelloVolatile();for(inti =0; i<10; i++){newThread(){publicvoidrun(){for(intj =0; j<1000; j++){ hello.mNumber ++; } } }.start(); }while(Thread.activeCount()>2){ Thread.yield(); } System.out.println("number:"+hello.mNumber); }}

這段代碼預期結果是10000,可是每次執行結果都有可能不一樣。這是因為自增或自減都是非原子操作。

(1) 假如mNumber此時等於100,線程1進行自增操作。

(2)線程1先讀取了mNumber的值100,然後它被堵塞了。

(3)這時候線程2讀取mNumber的值100,然後進行了自增操作,並寫入到主存中, 這時候主存中的值為101。

(4)這時候線程1繼續執行,因為此前線程1已經讀取到值100,然後進行自增操作101,然後將101寫入到主存中。

可以看到兩個線程分別對100進行了+1操作,預期主存中的nNumber = 102,實際mNumebr = 101; 這就是因為非原子操作造成的。

使用場景

(1)並發編程中不依賴於程序中任意其狀態的狀態標識。可以通過關鍵字volatile代替synchronized, 提高程序執行效率,並簡化代碼。

(2)單例模式的雙重檢查模式DCL

publicclassDclSingleton{privatevolatilestaticDclSingleton mInstance =null;publicstaticDclSingletongetInstance(){if(mInstance==null){synchronized(DclSingleton.class){if(mInstance==null){ mInstance =newDclSingleton(); } } }returnmInstance; }}

原理淺析

將volatile修飾的變量轉變成匯編代碼,如下:

... lock addl $0x0,(%rsp)

通過查IA-32架構安全手冊可知,Lock前綴指令在多核處理器會引發兩件事。

1)將當前處理器緩存行的數據寫回到系統內存。

2)這個寫回內存的操作會使在其他CPU裏緩存了該內存地址的數據無效。

解讀 :

為了提高,處理器不直接和內存進行通信,而是先將系統內存的數據讀到內部緩存後再進行操作,但操作完不知道何時再寫回內存。如果對聲明了volatile的變量進行寫操作,JVM會向處理機發送一條Lock前綴指令,將這個變量所在的緩存行的數據寫回到系統內存。

但是寫會內存後,如果其他處理器緩存的值還是舊的,再執行計算操作就會出現問題。所以在多處理器下,為了保證各個處理器緩存是一致的,就會實現緩存一致性協議,如下圖:

技術分享圖片

每個處理器通過嗅探在總線上傳播的數據來檢查自己緩存的數據是否過期了,當處理器發現自己的緩存行對應的內存地址被修改,就會將當前處理器的緩存行設置成無效狀態。當處理器對這個數據進行操作的時候,就會重新從系統內存中把數據讀到處理器緩存中。

文末福利:

想要了解更多並發編程知識點的,可以關註我一下,我後續也會整理更多關於並發編程這一塊的知識點分享出來,另外順便給大家推薦一個交流學習群:650385180,裏面會分享一些資深架構師錄制的視頻錄像:有Spring,MyBatis,Netty源碼分析,高並發、高性能、分布式、微服務架構的原理,JVM性能優化以及並發編程這些成為架構師必備的知識體系。還能領取免費的學習資源,目前受益良多,以下學習資源都在群的共享區。

技術分享圖片


幹貨:Java並發編程系列之volatile(一)