或者說,volatile解決什麼問題?html
我本身的總結:volatile解決多線程下變量訪問的內存可見性問題,用於線程間通訊。java
通訊怎能理解呢,線程A寫一個volatile變量,隨後線程B讀這個volatile變量,這個過程實質上是線程A經過
主內存向線程B發送消息。c++
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/javas...架構
大概意思是,java語言容許多個線程訪問共享變量。爲了保證共享變量能準確一致的更新,線程要保證經過鎖的機制單獨得到這個變量。java提供了一種機制,容許你把變量定義成volatile,在某些狀況下比直接用鎖更加方便。併發
若是一個變量被定義成volatile,java內存模型確保全部線程看到的這個共享變量是一致的。oracle
這個一致怎麼理解呢?繼續往下看。
先來看一幅圖,
這是一幅計算的內存架構圖。
如今的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修飾。
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關鍵字能夠保證內存可見性,指令執行的有序性。可是請必定記住,它無法保證原子性。舉個例子你可能比較容易明白。
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不保證原子性的最好證據。那麼深層次的緣由是什麼呢?
事實上,自增操做包括三個步驟:
既然分了三個步驟,就有可能出現下面這種狀況:
假如某個時刻變量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
同步鎖修飾。具體不展開了。
參考: