一、為什麼要用到volatile關鍵字?
使用一個新技術的原因肯定是當前存在了很多問題,在Java多線程的開發中有三種特性:原子性、可見性和有序性。我們可以在這裡簡單的說一下:
1、原子性(Atomicity)
原子性是指在一個操作中就是cpu不可以在中途暫停然後再調度,既不被中斷操作,要不執行完成,要不就不執行,就好比你做一件事,要麼不做,要麼做完。java提供了很多機制來保證原子性。我們舉一個例子,比如說常見的a++就不滿足原子性。這個操作實際是a = a + 1;是可分割的。在運行的時候可能做了一半不做了。所以不滿足原子性。
為了解決上面a++出現的問題,java提供了很多其他的關鍵字和類,比如說AtomicInteger、AtomicLong、AtomicReference等。
2、可見性(Visibility)
可見性就是指當一個線程修改了線程共享變量的值,其它線程能夠立即得知這個修改。如果我們學過java內存模型的話,對下面這張圖想必不陌生:
每一個線程都有一份自己的本地內存,所有線程共用一份主內存。如果一個線程對主內存中的數據進行了修改,而此時另外一個線程不知道是否已經發生了修改,就說此時是不可見的。
這種不可見的狀況會帶來一個問題,兩個線程有可能會操作同一份但是值不一樣的數據。這時候怎麼辦呢?於是乎,今天的主角登場了,這就是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。我們可以運行一下看看結果:
最後得出的結論就是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無處不在。