volatile關鍵字可以說是Java虛擬機提供的最輕量級的同步機制,但是它并不容易彎曲被正確、完整的理解。
volatile的特性
volatile關鍵字是Java虛擬機提供的最輕量級的同步機制。Java內存模型對volatile專門定義了一些特殊的訪問規則。
當一個變量被volatile修飾后,它將具備兩種特性:
1. 可見性
** 第一是保證此變量對所以線程的可見性** ,這里的"可見性"是指當一個線程修改了這個變量的值,新值對于其它線程來說是可以立即得知的。而普通變量不能做到這一點,普通變量的值在線程間傳遞均需要通過主內存來完成,例如,線程A修改一個普通的變量的值,然后向主內存進行回寫,另外一條線程B在線程A回寫完成了之后再從主內存進行讀取操作,新變量值才會對線程B可見。
關于volatile變量的可見性,經常會被開發人員誤解,認為以下描述成立:"volatile變量對所有線程是立即可見,對volatile變量所有的寫操作都能立刻反映到其他線程之中,換句話說,volatile變量在各個線程中是一致的,所以基于volatile變量的運算在并發情況下是安全的"。這句話論據部分并沒有錯,但是其論據并不能得出"基于volatile變量的運算在并發情況下是安全的"這個結論。volatile變量在各個線程的工作內存中不存在一致性問題(在各個線程的工作內存中,volatile變量也可以存在不一致的情況,但由于每次使用之前都要先刷新,執行引擎看不到不一致的情況,因此可以認為不存在一致性問題),但是Java里面的運算并非是原子操作,導致volatile變量的運算在并發情況下一樣是不安全的。
例如:
package com.bytebeats.codelab;
import java.util.concurrent.CountDownLatch;
/**
* ${DESCRIPTION}
*
* @author Ricky Fung
*/
public class VolatileDemo {
public static void main(String[] args) throws InterruptedException {
VolatileDemo demo = new VolatileDemo();
demo.run();
}
public void run() throws InterruptedException {
int threadNum = 20;
final CountDownLatch latch = new CountDownLatch(threadNum);
for (int i=0; i<threadNum; i++){
Thread t = new Thread(new Runnable() {
@Override
public void run() {
for(int x=0; x<50; x++){
incr();
}
latch.countDown();
}
});
t.start();
}
//等待所有線程執行完累加操作
latch.await();
System.out.println(race);
}
private volatile int race = 0;
private void incr(){
race++;
}
}
模擬了20個線程對race變量進行自增操作,如果這段代碼是并發安全的話,最后輸出的結果應該為1000。但事實上每次運行上述代碼,輸出的結果可能都會不一樣,都是一個小于1000的數。
究其原因,就是Java 中 race++
不是原子操作。
由于volatile變量,只能保證可見性,在不符合以下兩條規則的情況下,我們仍然需要通過加鎖(使用synchronized 或 java.util.concurrent中的原子類)來保證原子性。
- 運算結果并不依賴變量的當前值,或者能夠確保只有單一的線程修改變量的值。
- 變量不需要與其他的狀態變量共同參與不變約束。
2. 禁止指令重排序優化
使用volatile變量的第二個語義是禁止指令重排序優化,
原子性、可見性、有序性
Java內存模型是圍繞著在并發過程中如何處理 原子性、可見性和有序性 這3個特征來建立的。我們來逐個看一下哪些操作實現了這3個特性。
原子性
由Java內存模型來直接保證的原子性變量操作包括:read、load、assign、use、store和write,我們大致可以認為基本數據類型的訪問操作是具備原子性的(例外的是long和double的非原子性協定)。
如果硬要程序需要一個更大范圍的原子性保證(經常會遇到),Java內存模型還提供了synchronized 關鍵字和Lock,在synchronized 塊之間的操作也具備原子性。
可見性
可見性是指當一個線程修改了這個變量的值,新值對于其它線程來說是可以立即得知這個修改。
Java內存模型是通過在變量修改后將新值同步回主內存,在變量讀取前從主內存刷新變量值 這種依賴主內存作為傳遞媒介的方式來實現可見性的,無論是普通變量還是volatile變量都是如此,普通變量與volatile變量的區別是:volatile的特殊規則保證了新值能立即同步到主內存,以及每次使用前立即從主內存刷新。因此,可以說volatile保證了多線程操作時 變量的可見性,而普通變量則不能保證這一點。
除了volatile之外,Java還有兩個關鍵字能實現可見性,即synchronized和final。