Java里面volatile關鍵字主要有兩個作用:
(1)可見性
(2)禁止指令重排序
第一條可見性比較容易理解,就是使用volatile修飾的共享變量,如果有一個線程修改了值,其他的線程里面是立即可見的。原理是對volatile變量的讀寫,都會強制線程操作從主內存。
第二條禁止指令重排序,能夠保證局部的代碼執行的順序。假設我們現在有如下的一段代碼:
int a=2; int b=1;
從順序上看a應該先執行,而b會后執行,但實際上卻不一定是,因為cpu執行程序的時候,為了提高運算效率,所有的指令都是並發的亂序執行,如果a和b兩個變量之間沒有任何依賴關系,那么有可能是b先執行,而a后執行,因為不存在依賴關系,所以誰先誰后並不影響程序最終的結果。這就是所謂的指令重排序。 ok,接着我們繼續分析下面稍加改動后的代碼:
int a=2; int b=1; int c=a+b;
這段代碼里,不管a和b如何亂序執行,c的結果都是3,因為c變量依賴a和b變量,所以c變量是不會重排序到a或者b之前,a和b也不會重排到c之后,這其實是由happens-before關系里面的單線程下的as-if-serial語義限制的。
這里面還有一種特殊情況,需要注意一下:
int a = 1; int b = 2; try { a = 3; //A b = 1 / 0; //B } catch (Exception e) { } finally { System.out.println("a = " + a); }
上面的例子中a和b變量,雖然沒有依賴關系,但是在try-catch塊里面發生了重排,b先執行,然后發生了異常,那么a的值最終還是3,由JVM保證在重排序發生異常的時候,在catch塊里面作相關的特殊處理。這一點需要注意。
在單線程環境下,指令重排序是不會影響程序的最終執行結果的,但是重排序如果發生多線程環境下,就有可能影響程序正常執行,看下面的代碼:
public class ReorderDemo1 { private int count=2; private boolean flag=false; private volatile boolean sync=false; public void write1() { count=10; flag=true;//沒有volatile修飾,實際執行順序,有可能是flag=true先執行 } public void read1() { if(flag){ System.out.print(count); // 有些jvm會打印10,有些jvm會打印2,這是不確定的 } } public void write2() { count=10; sync=true;// 由於出現了volatile,所以這里禁止重排序 } public void read2() { if(sync){ System.out.print(count); // 在jdk5之后,由volatile保證,count的值總是等於10 } } public static void main(String[] args) { for(int i=0;i<300;i++){ //實例化變量 ReorderDemo1 reorderDemo1=new ReorderDemo1(); //寫線程 Thread t1=new Thread(()-> { reorderDemo1.write1();}); //讀線程 Thread t2=new Thread(()-> { reorderDemo1.read1(); }); t1.start(); t2.start(); } } }
上面的代碼里面,有三個成員變量,其中最后一個是用volatile修飾的,有2對方法:
第一對方法里面:
private int count=2; private boolean flag=false; private volatile boolean sync=false; public void write1() { count=10; flag=true;//沒有volatile修飾,實際執行順序,有可能是flag=true先執行 } public void read1() { if(flag){ System.out.print(count); // 有些jvm會打印10,有些jvm會打印2,這是不確定的 } }
上面的代碼,由於指令會重排序,當線程一里面執行write1方法的flag=true的時候,同時線程2執行了read1方法,那么count的值是不確定的,可能是10,也可能是2,這個其實和操作系統有很大關系,如果cpu不支持指令重排,那么就不會出現問題,比如在X86的CPU上運行代碼測試,可能不會出現多個值,但這不能說明其他的操作系統也不會出現。指令重排序在多線程環境下會帶來不確定性,想要正確的使用,需要理解JMM內存模型。
第二對方法里面:
private int count=2; private boolean flag=false; private volatile boolean sync=false; public void write2() { count=10; sync=true;// 由於出現了volatile,所以這里禁止重排序 } public void read2() { if(sync){ System.out.print(count); // 在jdk5之后,由volatile保證,count的值總是等於10 } }
注意這里的sync變量是加了volatile修飾,意味着禁止了重排序,第一個線程調用write2方法時候,同樣第二個線程在調用read2方法時候,如果sync=true,那么count的值一定是10,有朋友可能會說count變量沒有用volatile修飾啊,如何保證100%可見性呢? 確實在jdk5之前volatile關鍵字確實存在這種問題,必須都得加volatile修飾,但是在jdk5及以后修復了這個問題,也就是在jsr133里面增強了volatile關鍵字的語義,volatile變量本身可以看成是一個柵欄,能夠保證在其前后的變量也具有volatile語義,同時由於volatile的出現禁止了重排序,所以在多線程下仍然可以得到正確的結果。
總結:
在Java里面除了volatile有禁止重排序的功能,內置鎖synchronized和並發包的Lock也都有同樣的語義。同步手段解決的主要問題是要保證代碼執行的原子性,有序性,可見性。內置鎖和J.U.C的鎖同時具有這三種功能,而volatile不能保證原子性,所以在必要的時候還需要配合鎖一起使用,才能編寫出正確的多線程應用。