若是一個共享變量(類的成員變量、類的靜態成員變量)被volatile修飾以後,那麼就具有了兩層語義:html
在Java中long賦值不是原子操做,由於先寫32位,再寫後32位,分兩步操做,而AtomicLong賦值是原子操做,爲何?爲何volatile能替代簡單的鎖,卻不能保證原子性?這裏面涉及volatile,是java中的一個我以爲這個詞在Java規範中從未被解釋清楚的神奇關鍵詞,在Sun的JDK官方文檔是這樣形容volatile的:java
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.c++
意思就是說,若是一個變量加了volatile關鍵字,就會告訴編譯器和JVM的內存模型:這個變量是對全部線程共享的、可見的,每次jvm都會讀取最新寫入的值並使其最新值在全部CPU可見。volatile彷佛是有時候能夠代替簡單的鎖,彷佛加了volatile關鍵字就省掉了鎖。但又說volatile不能保證原子性(java程序員很熟悉這句話:volatile僅僅用來保證該變量對全部線程的可見性,但不保證原子性)。這不是互相矛盾嗎?程序員
volatile到底如何保證可見性和禁止指令重排序的。「觀察加入volatile關鍵字和沒有加入volatile關鍵字時所生成的彙編代碼發現,加入volatile關鍵字時,會多出一個lock前綴指令」(摘自《深刻理解Java虛擬機》)lock前綴指令實際上至關於一個內存屏障(也成內存柵欄),內存屏障會提供3個功能:緩存
例如你讓一個volatile的integer自增(i++);安全
mov 0xc(%r10),%r8d ; Load(讀取volatile變量值到local) inc %r8d ; Increment(增長變量的值) mov %r8d,0xc(%r10) ; Store(把local的值寫回,,讓其它的線程可見) lock addl $0x0,(%rsp) ; StoreLoad Barrier
內存屏障(memory barrier)是一個CPU指令。基本上,它是這樣一條指令: a) 確保一些特定操做執行的順序; b) 影響一些數據的可見性(多是某些指令執行後的結果)。編譯器和CPU能夠在保證輸出結果同樣的狀況下對指令重排序,使性能獲得優化。插入一個內存屏障,至關於告訴CPU和編譯器先於這個命令的必須先執行,後於這個命令的必須後執行。內存屏障另外一個做用是強制更新一次不一樣CPU的緩存。例如,一個寫屏障會把這個屏障前寫入的數據刷新到緩存,這樣任何試圖讀取該數據的線程將獲得最新值,而不用考慮究竟是被哪一個cpu核心或者哪顆CPU執行的。jvm
內存屏障(memory barrier)和volatile什麼關係?上面的虛擬機指令裏面有提到,若是你的字段是volatile,Java內存模型將在寫操做後插入一個寫屏障指令,在讀操做前插入一個讀屏障指令。這意味着若是你對一個volatile字段進行寫操做,你必須知道:一、一旦你完成寫入,任何訪問這個字段的線程將會獲得最新的值。二、在你寫入前,會保證全部以前發生的事已經發生,而且任何更新過的數據值也是可見的,由於內存屏障會把以前的寫入值都刷新到緩存。ide
volatile爲何沒有原子性?性能
明白了內存屏障(memory barrier)這個CPU指令,回到前面的JVM指令:從Load到store到內存屏障,一共4步,其中最後一步jvm讓這個最新的變量的值在全部線程可見,也就是最後一步讓全部的CPU內核都得到了最新的值,但中間的幾步(從Load到Store)是不安全的,中間若是其餘的CPU修改了值將會丟失。下面的測試代碼能夠實際測試voaltile的自增沒有原子性:測試
假如線程1先執行,線程2後執行:
//線程1 boolean stop = false; while(!stop){ doSomething(); } //線程2 stop = true;
這段代碼是很典型的一段代碼,不少人在中斷線程時可能都會採用這種標記辦法。可是事實上,這段代碼會必定會將線程中斷麼?不必定,也許在大多數時候,這個代碼可以把線程中斷,可是也有可能會致使沒法中斷線程(雖然這個可能性很小,可是隻要一旦發生這種狀況就會形成死循環了)。這是由於每一個線程在運行過程當中都有本身的工做內存,那麼線程1在運行的時候,會將stop變量的值拷貝一份放在本身的工做內存當中。那麼當線程2更改了stop變量的值以後,可是還沒來得及寫入主存當中,線程2轉去作其餘事情了,那麼線程1因爲不知道線程2對stop變量的更改,所以還會一直循環下去。
若是使用volatile修飾,第一,會強制將修改的值當即寫入主存;第二,當線程2進行修改時,會致使線程1的工做內存中緩存變量stop的緩存行無效(反映到硬件層的話,就是CPU的L1或者L2緩存中對應的緩存行無效);第三,因爲線程1的工做內存中緩存變量stop的緩存行無效,因此線程1再次讀取變量stop的值時會去主存讀取。那麼在線程2修改stop值時(固然這裏包括2個操做,修改線程2工做內存中的值,而後將修改後的值寫入內存),會使得線程1的工做內存中緩存變量stop的緩存行無效,而後線程1讀取時,發現本身的緩存行無效,它會等待緩存行對應的主存地址被更新以後,而後去對應的主存讀取最新的值。
public class Test { public volatile int inc = 0; public void increase() { inc++; } public static void main(String[] args) { final Test test = new Test(); for(int i=0;i<10;i++){ new Thread(){ public void run() { for(int j=0;j<1000;j++) test.increase(); }; }.start(); } while(Thread.activeCount()>1) //保證前面的線程都執行完 Thread.yield(); System.out.println(test.inc); } }
程序的運行結果是一個小於10000的數字。volatile的可見性只能保證每次讀取的是最新的值,可是volatile沒辦法保證對變量的操做的原子性。在前面已經提到過,自增操做是不具有原子性的,它包括讀取變量的原始值、進行加1操做、寫入工做內存。那麼就是說自增操做的三個子操做可能會分割開執行,就有可能致使下面這種狀況出現:
假如某個時刻變量inc的值爲10,線程1對變量進行自增操做,線程1先讀取了變量inc的原始值,而後線程1被阻塞了;而後線程2對變量進行自增操做,線程2也去讀取變量inc的原始值,因爲線程1只是對變量inc進行讀取操做,而沒有對變量進行修改操做,因此不會致使線程2的工做內存中緩存變量inc的緩存行無效,因此線程2會直接去主存讀取inc的值,發現inc的值時10,而後進行加1操做,並把11寫入工做內存,最後寫入主存。而後線程1接着進行加1操做,因爲已經讀取了inc的值,注意此時在線程1的工做內存中inc的值仍然爲10(由於線程1並無再次讀取Iinc的值,而可見性只能保證每次讀取的是最新的值),因此線程1對inc進行加1操做後inc的值爲11,而後將11寫入工做內存,最後寫入主存。那麼兩個線程分別進行了一次自增操做後,inc只增長了1。
把上面的代碼改爲如下任何一種均可以達到效果:
採用synchronized:
public class Test { public int inc = 0; public synchronized void increase() { inc++; } public static void main(String[] args) { final Test test = new Test(); for(int i=0;i<10;i++){ new Thread(){ public void run() { for(int j=0;j<1000;j++) test.increase(); }; }.start(); } while(Thread.activeCount()>1) //保證前面的線程都執行完 Thread.yield(); System.out.println(test.inc); } }
採用Lock:
public class Test { public int inc = 0; Lock lock = new ReentrantLock(); public void increase() { lock.lock(); try { inc++; } finally{ lock.unlock(); } } public static void main(String[] args) { final Test test = new Test(); for(int i=0;i<10;i++){ new Thread(){ public void run() { for(int j=0;j<1000;j++) test.increase(); }; }.start(); } while(Thread.activeCount()>1) //保證前面的線程都執行完 Thread.yield(); System.out.println(test.inc); } }
採用AtomicInteger:
public class Test { public AtomicInteger inc = new AtomicInteger(); public void increase() { inc.getAndIncrement(); } public static void main(String[] args) { final Test test = new Test(); for(int i=0;i<10;i++){ new Thread(){ public void run() { for(int j=0;j<1000;j++) test.increase(); }; }.start(); } while(Thread.activeCount()>1) //保證前面的線程都執行完 Thread.yield(); System.out.println(test.inc); } }
在java 1.5的java.util.concurrent.atomic包下提供了一些原子操做類,即對基本數據類型的 自增(加1操做),自減(減1操做)、以及加法操做(加一個數),減法操做(減一個數)進行了封裝,保證這些操做是原子性操做。atomic是利用CAS來實現原子性操做的(Compare And Swap),CAS其實是利用處理器提供的CMPXCHG指令實現的,而處理器執行CMPXCHG指令是一個原子性操做。
在前面提到volatile關鍵字能禁止指令重排序,因此volatile能在必定程度上保證有序性。volatile關鍵字禁止指令重排序有兩層意思:
1)當程序執行到volatile變量的讀操做或者寫操做時,在其前面的操做的更改確定所有已經進行,且結果已經對後面的操做可見;在其後面的操做確定尚未進行;
2)在進行指令優化時,不能將在對volatile變量訪問的語句放在其後面執行,也不能把volatile變量後面的語句放到其前面執行。
可能上面說的比較繞,舉個簡單的例子:
//x、y爲非volatile變量 //flag爲volatile變量 x = 2; //語句1 y = 0; //語句2 flag = true; //語句3 x = 4; //語句4 y = -1; //語句5
因爲flag變量爲volatile變量,那麼在進行指令重排序的過程的時候,不會將語句3放到語句一、語句2前面,也不會講語句3放到語句四、語句5後面。可是要注意語句1和語句2的順序、語句4和語句5的順序是不做任何保證的。而且volatile關鍵字能保證,執行到語句3時,語句1和語句2一定是執行完畢了的,且語句1和語句2的執行結果對語句三、語句四、語句5是可見的。
那麼咱們回到前面舉的一個例子:
//線程1: context = loadContext(); //語句1 inited = true; //語句2 //線程2: while(!inited ){ sleep() } doSomethingwithconfig(context);
前面舉這個例子的時候,提到有可能語句2會在語句1以前執行,那麼久可能致使context還沒被初始化,而線程2中就使用未初始化的context去進行操做,致使程序出錯。這裏若是用volatile關鍵字對inited變量進行修飾,就不會出現這種問題了,由於當執行到語句2時,一定能保證context已經初始化完畢。
volatile關鍵字在某些狀況下性能要優於synchronized,可是要volatile關鍵字是沒法替代synchronized關鍵字的,由於volatile關鍵字沒法保證操做的原子性。一般來講,不要將volatile用在getAndOperate場合(這種場不是原子,須要再加鎖),僅僅set或者get的場景是適合volatile的。
下面列舉幾個Java中使用volatile的幾個場景。
1.狀態標記量
volatile boolean flag = false; while(!flag){ doSomething(); } public void setFlag() { flag = true; }
volatile boolean inited = false; //線程1: context = loadContext(); inited = true; //線程2: while(!inited ){ sleep() } doSomethingwithconfig(context);
2.double check
class Singleton{ private volatile static Singleton instance = null; private Singleton() { } public static Singleton getInstance() { if(instance==null) { synchronized (Singleton.class) { if(instance==null) instance = new Singleton(); } } return instance; } }