【Java基礎】:volatile實現可見性的原理

1. 引言

在java併發編程中,一定繞不開volatile、synchronized和lock幾個關鍵字,其中volatile關鍵字是用來解決共享變量(類成員變量、類的靜態成員變量等)的可見性問題的,非共享變量(方法的局部變量)是分配在JVM虛擬機的棧中,是線程私有的,不涉及可見性問題。那麼什麼是可見性?

2. 什麼叫做可見性

可見性:在JAVA規範中是這樣定義的:java編程語言允許線程訪問共享變量,爲了確保共享變量能被準確和一致地更新,線程應該確保通過排他鎖單獨獲得這個變量。通俗的將就是如果有一個共享變量N,當有兩個線程T1、T2同時獲取了N的值,T1修改N的值,而T2讀取N的值。那麼可見性規範要求T2讀取到的必須是T1修改後的值,而不能在T2讀取舊值後T1修改爲新值。volatile關鍵字修飾的共享變量可以提供這種可見性規範,也叫做讀寫可見。那麼底層實現是通過機制保證volatile變量讀寫可見的?

3. Volatile的實現機制

在說這個問題之前,我們先看看CPU是如何執行java代碼的。
在這裏插入圖片描述
首先編譯之後Java代碼會被編譯成字節碼.class文件,在運行時會被加載到JVM中,JVM會將.class轉換爲具體的CPU執行指令,CPU加載這些指令逐條執行。
在這裏插入圖片描述
以多核CPU爲例(兩核),我們知道CPU的速度比內存要快得多,爲了彌補這個性能差異,CPU內核都會有自己的高速緩存區,當內核運行的線程執行一段代碼時,首先將這段代碼的指令集進行緩存行填充到高速緩存,如果非volatil變量當CPU執行修改了此變量之後,會將修改後的值回寫到高速緩存,然後再刷新到內存中。如果在刷新會內存之前,由於是共享變量,那麼CORE2中的線程執行的代碼也用到了這個變量,這是變量的值依然是舊的。volatile關鍵字就會解決這個問題的,如何解決呢,首先被volatile關鍵字修飾的共享變量在轉換成彙編語言時,會加上一個以lock爲前綴的指令,當CPU發現這個指令時,立即做兩件事:

  1. 將當前內核高速緩存行的數據立刻回寫到內存;
  2. 使在其他內核裏緩存了該內存地址的數據無效。

第一步很好理解,第二步如何做到呢?
MESI協議:在早期的CPU中,是通過在總線加LOCK#鎖的方式實現的,但這種方式開銷太大,所以Intel開發了緩存一致性協議,也就是MESI協議,該解決緩存一致性的思路是:當CPU寫數據時,如果發現操作的變量是共享變量,即在其他CPU中也存在該變量的副本,那麼他會發出信號通知其他CPU將該變量的緩存行設置爲無效狀態。當其他CPU使用這個變量時,首先會去嗅探是否有對該變量更改的信號,當發現這個變量的緩存行已經無效時,會從新從內存中讀取這個變量。

4. 使用volatile的好處

從底層實現原理我們可以發現,volatile是一種非鎖機制,這種機制可以避免鎖機制引起的線程上下文切換和調度問題。因此,volatile的執行成本比synchronized更低。

5. volatile的不足

使用volatile關鍵字,可以保證可見性,但是卻不能保證原子操作,例如:

public class TestVolatile {
    public volatile int inc = 0;
    public void increase() {
        inc++;
    }
 
    public static void main(String[] args) {
        final Test test = new Test();
        for(int i=0;i<10;i++){
            new Thread(){
                public void run() {
                    for(int j=0;j<1000;j++)
                        test.increase();
                };
            }.start();
        }
 
        while(Thread.activeCount()>1)
            Thread.yield();
        System.out.println(test.inc);
    }
}

這裏我們用10個線程,每個線程+1000,預期應該是10000,實際上編譯執行這段代碼,輸出值都會小於10000。爲什麼會這樣?因爲,自增操作並不是原子操作,它含括讀,加1,寫入工作內存三步操作。這三步是分開操作的,當inc的值爲5,thead1執行自增操作,當thread1讀到5之後,還沒有來得及寫入就被阻塞了,那麼thead2讀取的依然是原值。所以volatile的非鎖機制只能保證修飾的變量的可見性,而當對變量進行非原子操作時,volatile就無法保證了。這種時候就需要使用synchronzied或lock。

參考

  1. https://blog.csdn.net/nch_ren/article/details/78924808
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章