開發與維運

詳解java中的併發關鍵字volatile

一、為什麼要用到volatile關鍵字?


使用一個新技術的原因肯定是當前存在了很多問題,在Java多線程的開發中有三種特性:原子性、可見性和有序性。我們可以在這裡簡單的說一下:

1、原子性(Atomicity)


原子性是指在一個操作中就是cpu不可以在中途暫停然後再調度,既不被中斷操作,要不執行完成,要不就不執行,就好比你做一件事,要麼不做,要麼做完。java提供了很多機制來保證原子性。我們舉一個例子,比如說常見的a++就不滿足原子性。這個操作實際是a = a + 1;是可分割的。在運行的時候可能做了一半不做了。所以不滿足原子性。

為了解決上面a++出現的問題,java提供了很多其他的關鍵字和類,比如說AtomicInteger、AtomicLong、AtomicReference等。

2、可見性(Visibility)


可見性就是指當一個線程修改了線程共享變量的值,其它線程能夠立即得知這個修改。如果我們學過java內存模型的話,對下面這張圖想必不陌生:

v2-f70a54bbe8b655383c163e7e9727d07f_1440w.jpg

每一個線程都有一份自己的本地內存,所有線程共用一份主內存。如果一個線程對主內存中的數據進行了修改,而此時另外一個線程不知道是否已經發生了修改,就說此時是不可見的。

這種不可見的狀況會帶來一個問題,兩個線程有可能會操作同一份但是值不一樣的數據。這時候怎麼辦呢?於是乎,今天的主角登場了,這就是volatile關鍵字。

volatile關鍵字的作用很簡單,就是一個線程在對主內存的某一份數據進行更改時,改完之後會立刻刷新到主內存。並且會強制讓緩存了該變量的線程中的數據清空,必須從主內存重新讀取最新數據。這樣一來就保證了可見性

3、有序性

程序執行的順序按照代碼的先後順序執行就叫做有序性,但是有時候程序的執行並不會遵循,比如說下面的代碼:

int i = 1;              
int j = 2;

這兩行代碼很簡單,i=1,j=2,程序在運行的時候一定會先讓i=1,然後j=2嘛?不一定,為什麼會不一定,這是因為有可能會發生指令重排序,從名字看就知道,在運行的時候,代碼會重新排列。這裡面涉及到的就比較多了。我會在專門的文章中進行講解。

為了防止上面的重排序,java依然提供了很多機制,比如volatile關鍵字等。這也是我們volatile關鍵字第二個使用的場景。

在上面我們從java併發編程的三個特徵來分析了為什麼會用到volatile關鍵字,主要是保證內存可見性和防止指令重排序。下面我們就來正式來分析一下這個volatile。

二、深入剖析


1、volatile保證原子性嘛?


在上面我們只說了volatile關鍵字會保證可見性和有序性,但是並沒有說會不會保證原子性,原子性的概念我們已經說了,也就是一個操作,要麼不執行,要麼執行到底。我們可以使用代碼來驗證一下:

public class Test {
    private static volatile int a = 0;
    public static void main(String[] args) {
        Test test = new Test();
        Thread[] threads = new Thread[5];
        for (int i = 0; i < 5; i++) {
            threads[i] = new Thread(() -> {
                try {
                    for (int j = 0; j < 10; j++) {
                        System.out.println(++a);
                        Thread.sleep(500);
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                }
            });
            threads[i].start();
        }
    }
}

這段代碼的含義是,有5個線程,每一個線程都對a進行遞增。每個線程一次加10個數。按道理來講,如果volatile關鍵字保證原子性的話,最後結果一定是50。我們可以運行一下看看結果:

v2-256d5bb3cf520b610bdd6e653e17af62_1440w.jpg

最後得出的結論就是volatile不保證原子性。既然不能保證原子性,那肯定就是非線程安全的。

2、單例模式的雙重鎖為什麼要加volatile?


什麼是雙重鎖的單例模式,我們給出代碼可以看看。

public class Test2 {
    private static volatile Test2 test2;
    public static Test2 getInstance() {
        if(test2 == null) {
            synchronized (Test2.class) {
                if(test2 == null) {
                    test2 = new Test2();
                }
            }
        }
        return test2;
    }
}

這就是單例模式的雙重鎖實現,為什麼這裡要加volatile關鍵字呢?我們把test2 = new Test2()這行代碼進行拆分。可以分解為3個步驟:

(1)memory=allocate();// 分配內存

(2)ctorInstanc(memory) //初始化對象

(3)test2=memory //設置s指向剛分配的地址

如果沒有volatile關鍵字,可能會發生指令重排序。在編譯器運行時,從1-2-3 排序為1-3-2。此時兩個線程同時進來的時候出現可見性問題,也就是說一個線程執行了1-3,另外一個線程一進來直接返回還未執行2的null對象。而我們的volatile關鍵之前已經說過了,可以很好地防止指令重排序。也就不會出現這個問題了。

如果我們學過java併發系列的其他類比如說Atomic等,通過源碼我們會發現volatile無處不在。

Leave a Reply

Your email address will not be published. Required fields are marked *