Java多執行緒之volatile

NO IMAGE

Java多執行緒是一個龐大的知識體系,這裡對其中的volatile進行一個總結,理清他的來龍去脈。

CPU快取

要搞懂volatile,首先得了解CPU在執行過程中的儲存是如何處理的,其結構如圖

clipboard.png

CPU會把一些經常使用的資料快取在cache中,避免每次都去訪問較慢的memory。在單執行緒環境下,如果一個變數的修改都在cache中,自然不會有什麼問題,可是在多執行緒環境中就可能是下面這個圖的示意圖(單核另當別論)

clipboard.png

CPU1 修改了一個變數a存入cache1,但是CPU2 在cache2中看到的a任然是之前的a,所以造成CPU1修改失效,我們來看看示例程式碼:

import java.util.concurrent.TimeUnit;

public class Counter {
    private static  boolean stop ;
    //private static volatile boolean stop ;
    public static void main(String[] args) throws Exception {
        Thread t = new Thread(new Runnable() {
            @Override
            public void run() {
                int i = 0;
                while (!stop) {
                    i  ;
                }
            }
        } );
        t.start();

        TimeUnit.MILLISECONDS .sleep(5);
        stop = true;
    }
}

在我的4核筆記本上執行結果:

clipboard.png

就一直執行著,沒有停止(需要手工停止),這說明在主執行緒中修改的stop變數後,執行緒t沒有讀取到最新的stop的值,還一直是false。

volatile原理

volatile的原理就是,如果CPU1修改了一個變數a,不僅要修改自身的cache,還要同步到memory中去,並且使CPU2的cache中的變數a失效,如果CPU2要讀取a,那麼就必須到memory中去讀取,這樣就保證了不同的執行緒之間對於a的可見性,亦即,無論哪個執行緒,隨時都能獲得變數a最新的最新值。
我們來看看示例程式碼:

import java.util.concurrent.TimeUnit;

public class Counter {
    //private static  boolean stop ;
    private static volatile boolean stop ;
    public static void main(String[] args) throws Exception {
        Thread t = new Thread(new Runnable() {
            @Override
            public void run() {
                int i = 0;
                while (!stop) {
                    i  ;
                }
            }
        } );
        t.start();

        TimeUnit.MILLISECONDS .sleep(5);
        stop = true;
    }
}

在我的4核筆記本上執行結果:

clipboard.png

很快程式就結束了,說明執行緒t讀到了經主執行緒修改後的stop變數,然後就停止了。

(例子源於《effective Java》)

volatile使用場景

狀態標誌

就像上面的程式碼裡,把簡單地volatile變數作為狀態標誌,來達成執行緒之間通訊的目的,省去了用synchronized還要wait,notify或者interrupt的編碼麻煩。

替換重量級鎖

在Java中synchronized 又稱為重量級鎖,能夠保重JMM的幾大特性:一致性,原子性,可見性。但是由於使用了鎖操作,在一定程度上會有更高的效能消耗(鎖的執行緒互斥性亦即資源消耗)。而volatile能提供可見性,原子性(單個變數操作,不是a 這種符合操作),所以在讀寫上,可以用volatile來替換synchronized的讀操作,而寫操作仍然有synchronized實現,能取得更好的效能。

import java.util.ArrayList;
import java.util.List;

public class Counter1 {

    private class Count11 {
        private  int value;
        public synchronized int getValue() {
            return value;
        }
        public synchronized int increment() {
            return value  ;
        }
    }

//    private class Count11 {
//        private volatile int value=0;
//        int getValue() {  return value;    }
//        synchronized int increment() {    return value  ;    }
//    }

    public static void main(String[] args) throws Exception {
        Counter1.Count11 count11 = new Counter1().new Count11();
        List<Thread> threadArrayList = new ArrayList<>();
        final int[] a = {0};
        Long allTime = 0l;
        long startTime = System.currentTimeMillis();
        for (int i = 0; i < 4; i  ) {
            Thread t = new Thread(() -> {
                int b = 0;
                for (int j = 0; j < 10000; j  ) {
                    count11.increment();
                    a[0] = count11.getValue();
                }
                for (int j = 0; j < 10000; j  ) {
                    b  ;
                    a[0] = count11.getValue();
                }
            });
            t.start();
            threadArrayList.add(t);
        }
        for (Thread t : threadArrayList) {
            try {
                t.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        long endTime = System.currentTimeMillis();
        allTime = ((endTime - startTime));
        System.out.println("result: "   a[0]   ", average time: "   (allTime)   "ms");



    }

}

volatile優化結果:

result: 40000, average time: 124ms
result: 40000, average time: 133ms
result: 40000, average time: 141ms
result: 40000, average time: 112ms
result: 40000, average time: 123ms
result: 40000, average time: 143ms
result: 40000, average time: 120ms
result: 40000, average time: 120ms

未優化結果:

result: 40000, average time: 144ms
result: 40000, average time: 150ms
result: 40000, average time: 149ms
result: 40000, average time: 165ms
result: 40000, average time: 134ms
result: 40000, average time: 132ms
result: 40000, average time: 157ms
result: 40000, average time: 138ms
result: 40000, average time: 158ms

可見使用volatile過後效果的確優於只使用synchronized的效能,不過試驗中發現有個閾值,如果讀取修改次數較小,比如1000以內,只使用synchronized效果略好,存取次數變大以後 volatile的優勢才慢慢體現出來(次數達到10000的話,差距就在60ms左右)。

待挖掘

還有很多用法,在將來的學習中,不斷總結與挖掘。

聯想

無論處於應用的哪一層,優化的思路都是可以相互借鑑的,比如我們做一個服務叢集,如果每一個節點都要儲存所有使用者的session,就很難使得session同步,我們就可以借鑑volatile這種思路,在叢集之上搞一個排程器,如果某一個節點修改了一個使用者session,就報告給排程器,然後排程器通知其他所有節點修改該使用者session。而一般情況下,資料的讀寫比都比較高,所以這樣做就能到達一個很好的效能。

注意事項

引用型別的volatile只在引用本身發生變化時具有可見性,其引用的物件的元素髮生變化時不具有可見性

歡迎訪問我的個人主頁 mageek(mageek.cn)