volatile的概念
或者說,volatile解決什麼問題?
我自己的總結:volatile解決多線程下變量訪問的內存可見性問題,用於線程間通信。
通信怎能理解呢,線程A寫一個volatile變量,隨後線程B讀這個volatile變量,這個過程實質上是線程A通過
主內存向線程B發送消息。
java語言標準規範對volatile的描述是這樣的:
The Java programming language allows threads to access shared variables (§17.1). As a rule, to ensure that shared variables are consistently and reliably updated, a thread should ensure that it has exclusive use of such variables by obtaining a lock that, conventionally, enforces mutual exclusion for those shared variables.
The Java programming language provides a second mechanism, volatile fields, that is more convenient than locking for some purposes.
A field may be declared volatile, in which case the Java Memory Model ensures that all threads see a consistent value for the variable (§17.4).
上面這段話摘自這個鏈接,有興趣的可以自己點開看。
https://docs.oracle.com/javase/specs/jls/se8/html/jls-8.html#jls-8.3.1.4
大概意思是,java語言允許多個線程訪問共享變量。爲了保證共享變量能準確一致的更新,線程要保證通過鎖的機制單獨獲得這個變量。java提供了一種機制,允許你把變量定義成volatile,在某些情況下比直接用鎖更加方便。
如果一個變量被定義成volatile,java內存模型確保所有線程看到的這個共享變量是一致的。
這個一致怎麼理解呢?繼續往下看。
volatile詳解
先來看一幅圖,
這是一幅計算的內存架構圖。
現在的CPU大部分都是多核的,在計算機內部,變量讀寫的流程是這樣的:
- 當一個處理器需要讀取變量的時候,首先會把變量從主內存讀到緩存,也有可能是寄存器,然後再做各種計算。
- 計算的結果由寄存器刷新到緩存,然後再由緩存刷新到主內存。
- 一個處理器的緩存回寫到內存會導致其他處理器的緩存無效,這樣其它處理器
這裏的一個關鍵點是,什麼時候刷新?答案是不知道。我們不能假設CPU什麼時候會刷新。這樣就會帶來一些問題,比如一個線程寫完一個共享變量,還沒有刷新到主內存。然後另一個線程讀這個變量還是舊的值,在很多場景下,這個結果和程序員期望的並不一致。
幸運的是,我們雖然不知道CPU什麼時候刷新,但是我們可以強制CPU執行刷新。
再來看一個圖,這是JAVA的內存模型圖。
本地內存是JVM裏一個抽象的概念,它可以涵蓋寄存器,緩存等。
我們把這兩幅圖對應起來,可以這樣解釋。
在JAVA中,當一個線程寫變量時,會先把這個變量從主內存拷貝一份線程的本地內存,然後在本地內存操作。操作完成之後,再刷新到主內存。只有刷新後,另一個線程才能讀取新的值。
來看個例子:
public class VolatileTest implements Runnable {
private boolean running = true;
@Override
public void run() {
if (running) {
System.out.println("I am running");
}
}
public void stop() {
running = false;
}
}
這段代碼在多線程環境下執行的時候,假設A線程正在執行run
方法,B線程執行了stop
方法,我們的程序沒法保證A線程什麼時候會馬上停止。因爲這取決於CPU什麼時候進行刷新,把最新變量的值同步到主內存。
解決方法是,把running
這個共享變量用volatile修飾即可,這樣可以保證B線程的修改會立刻刷新到主內存,對其它線程可見。
public class VolatileTest implements Runnable {
private volatile boolean running = true;
再來看個稍微複雜一點的例子。
public class VolatileTest {
public volatile int a = 0;
volatile boolean flag = false;
public void write() {
a = 1; // 位置1
flag = true; //// 位置2
}
public void read() {
if (flag) { // 位置3
int i = a; // 位置4
}
}
}
Java規範對於volatile變量規則是:對一個volatile域的寫,happens-before於任意後續對這個volatile域的
讀。
假設線程A執行writer()方法之後,線程B執行reader()方法。根據volatile變量的happens-before規則,位置2必然先於位置3執行。同時我們知道在同一個線程中,所有操作必須按照程序的順序來執行,所以位置1肯定早於位置2,位置3早於位置4。然後我們能推出位置1早於位置4。
這樣的順序是符合我們預期的。
這裏A線程寫一個volatile變量後,B線程讀同一個volatile變量。A線程在寫volatile變量之
前所有可見的共享變量,在B線程讀同一個volatile變量後,將立即變得對B線程可見。
什麼時候需要使用volatile
通過上面的例子,我們可以總結下volatile的使用場景。
通常是,存在一個或者多個共享變量,會有線程對他們寫操作,也會有其它線程對他們讀操作。這樣的變量都應該使用volatile修飾。
volatile在標準庫裏的應用
ConcurrentHashMap裏用到了一些volatile的操作,比如:
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
volatile V val;
volatile Node<K,V> next;
Node(int hash, K key, V val, Node<K,V> next) {
this.hash = hash;
this.key = key;
this.val = val;
this.next = next;
}
...
可以看到,用於存儲值的value變量就是volatile類型,這樣可以保證在多線程讀取的時候,不會讀到過期的值。之所以不會讀到過期的值,是因爲根據Java內存模型的happen before原則,對volatile字段的寫入操作先於讀操作,即使兩個線程同時修改和獲取volatile變量,get操作也能拿到最新的值,這是用volatile替換鎖的經典應用場景。
volatile會降低程序執行的效率
不要過度使用volatile,不必要的場景沒有必要用volatile修飾變量,儘管這樣做程序也不會出什麼錯。
根據前面的描述,volatile相當於給變量的操作加了“鎖”,每次操作都有加鎖和釋放鎖的動作,效率自然會受影響。
volatile不是萬能的
對volatile經常有一中誤解就是,它可以保證原子操作。
通過上面的例子,我們知道,volatile關鍵字可以保證內存可見性,指令執行的有序性。但是請一定記住,它沒法保證原子性。舉個例子你可能比較容易明白。
public class VolatileTest {
public volatile int inc = 0;
public void increase() {
inc++;
}
public static void main(String[] args) {
final VolatileTest test = new VolatileTest();
for(int i=0;i<10;i++){
new Thread(() -> {
for(int j=0;j<1000;j++)
test.increase();
}).start();
}
while(Thread.activeCount()>2) //保證前面的線程都執行完
Thread.yield();
System.out.println(test.inc);
}
}
執行這段代碼,會發現結果每次一般都不同,但是肯定都小於10*1000。這就是volatile不保證原子性的最好證據。那麼深層次的原因是什麼呢?
事實上,自增操作包括三個步驟:
- 讀取變量的原始值
- 進行加1操作
- 寫入線程工作內存
既然分了三個步驟,就有可能出現下面這種情況:
假如某個時刻變量inc的值爲10。
第一步,線程1對變量進行自增操作,線程1先讀取了變量inc的原始值,然後線程1被阻塞了;
第二步, 然後線程2對變量進行自增操作,線程2也去讀取變量inc的原始值,由於線程1只是對變量inc進行讀取操作,而沒有對變量進行修改操作,所以不會導致線程2會直接去主存讀取inc的值,此時inc的值時10;
第三步, 線程2進行加1操作,並把11寫入工作內存,最後寫入主存。
第四步,線程1接着進行加1操作,由於已經讀取了inc的值,此時在線程1的工作內存中inc的值仍然爲10,所以線程1對inc進行加1操作後inc的值爲11,然後將11寫入工作內存,最後寫入主存。
最後,兩個線程分別進行了一次自增操作後,但是inc只增加了1。
有很多人會在第三步和第四步那裏有疑問,線程2更新inc的值以後,不是會導致線程1工作內存中的值失效嗎?
答案是不會,因爲在一個操作中,值只會讀取一次。這個是原子性和可見性區分的核心。
解決方案是使用increase方法使用synchronized
同步鎖修飾。具體不展開了。
參考:
- 《java併發編程的藝術》
- https://www.cnblogs.com/dolphin0520/p/3920373.html