Java并發編程之volatile關鍵字解析
volatile關鍵字雖然從字面上理解起來比較簡單,但是要用好不是一件容易的事情。本文我們就從JVM內存模型開始,了解一下 volatile 的應用場景。
volatile關鍵字
JVM內存模型
在了解 volatile 之前,我們有必要對JVM的內存模型有一個基本的了解。Java的內存模型規定了所有的變量都存儲在主內存中(即物理硬件的內存),每條線程還具有自己的工作內存(工作內存可能位于處理器的高速緩存之中),線程的工作內存中保存了該線程使用到的變量的主內存副本拷貝,線程對變量的所有操作(讀取,賦值等)都必須在工作內存中進行,而不能直接讀寫主內存中的變量)。不同的線程之間無法直接訪問對方工作內存之間的變量,線程間變量值的傳遞需要通過主內存來完成。
p.s: 對于上面提到的副本拷貝,比如假設線程中訪問一個10MB的對象,并不會把這10MB的內存復制一份拷貝出來,實際上這個對象的引用,對象中某個在線程訪問到的字段是有可能存在拷貝的,但不會有虛擬機實現把整個對象拷貝一次。
在并發編程中,我們通常會遇到以下三個問題:原子性,可見性,有序性,下面我們我們來具體看一下這三個特性與 volatile 之間的聯系:
有序性
public class Testcase{
public static int number;
public static boolean isinited;
public static void main(String[] args){
new Thread(
() -> {
while (!isinited) {
Thread.yield();
}
System.out.println(number);
}
).start();
number = 20;
isinited = true;
}
}
對于上面的代碼我們上面的本意是想輸出 20 ,但是如果運行的話可以發現輸出的值可能會是 0 。這是因為有時候為了提供程序的效率,JVM會做進行及時編譯,也就是可能會對指令進行重排序,將 isInited = true; 放在 number = 20; 之前執行,在單線程下面這樣做沒有任何問題,但是在多線程下則會出現重排序問題。如果我們將 number 聲名為 volatile 就可以很好的解決這個問題,這可以禁止JVM進行指令重排序,也就意味著 number = 20; 一定會在 isInited = true 前面執行。
可見性
比如對于變量 a ,當線程一要修改變量a的值,首先需要將a的值從主存復制過來,再將a的值加一,再將a的值復制回主存。在單線程下面,這樣的操作沒有任何的問題,但是在多線程下面,比如還有一個線程二,在線程一修改a的值的時候,也從主存將a的值復制過來進行加一,隨后線程一和線程二先后將a的值復制回主存,但是主存中a的值最終將只會加一而不是加二。
使用 volatile 可以解決這個問題,它可以保證在線程一修改a的值之后立即將修改值同步到主存中,這樣線程二拿到的a的值就是線程一已經修改過的a的值了。
原子性
原子性是指CPU在執行一條語句的時候,不會中途轉去執行另外的語句。比如 i = 1 就是一個原子操作,但是 ++i 就不是一個原子操作了,因為它要求首先讀取 i 的值,然后修改 i 的值,最后將值寫入主存中。
但是 volatile 卻不能保證程序的原子性,下面我們通過一個實例來驗證一下:
public class TestCase{
public volatile int v = 0;
public static final int threadCount = 20;
public void increase(){
v++;
}
public static void main(String[] args){
TestCase testCase = new TestCase();
for (int i=0; i<threadCount; i++) {
new Thread(
() -> {
for (int j=0; j<1000; j++) {
testCase.increase();
}
}
).start();
}
while (Thread.activeCount() > 1) {
Thread.yield();
}
System.out.println(testCase.v);
}
}
輸出結果:
上面我們的本意是想讓輸出 20000 ,但是運行程序后,結果可能會小于 20000 。因為 v++ 它本身并不是一個原子操作,它是分為多個步驟的,而且 volatile 本身也并不能保證原子性。
上面的程序使用 synchronzied 則可以很好的解決,只需要聲明 public synchronized void increase() 就行了。
或者使用lock也行:
Lock lock = new ReentrantLock();
public void increase(){
lock.lock();
try {
v++;
} finally{
lock.unlock();
}
}
或者將 v 聲明為 AtomicInteger v = new AtomicInteger(); 。在java 1.5的java.util.concurrent.atomic包下提供了一些原子操作類,即對基本數據類型的自增,自減,以及加法操作,減法操作進行了封裝,保證這些操作是原子性操作。
volatile的應用場景
下面我們通過單例模式來看一下 volatile 的一個具體應用:
class Singleton{
private volatile static Singleton instance;
public static Singleton getInstance(){
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null)
instance = new Singleton();
}
}
return instance;
}
public static void main(String[] args){
Singleton.getInstance();
}
}
上面 instance 必須要用 volatile 修飾,因為 new Singleton 是分為三個步驟的:
- 給instance指向的對象分配內存,并設置初始值為null(根據JVM類加載機制的原理,對于靜態變量這一步應該在 new Singleton 之前就已經完成了)。
- 執行構造函數真正初始化instance
- 將instance指向對象分配內存空間(分配內存空間之后instance就是非null了)
在我們的步驟2, 3之間的順序是可以顛倒的,如果線程一在執行步驟3之后并沒有執行步驟2,但是被線程二搶占了,線程二得到的 instance 是非null,但是instance卻還沒有初始化。而使用instance則可以保證程序的有序性。
References
來自:http://www.ziwenxie.site/2017/04/24/java-multithread-volatile/