volatile是變量修飾符,其修飾的變量具備內存可見性。java
可見性也就是說一旦某個線程修改了該被volatile修飾的變量,它會保證修改的值會當即被更新到主存,當有其餘線程須要讀取時,能夠當即獲取修改以後的值。c++
在Java中爲了加快程序的運行效率,對一些變量的操做一般是在該線程的寄存器或是CPU緩存上進行的,以後纔會同步到主存中,而加了volatile修飾符的變量則是直接讀寫主存。緩存
實例講解:併發
class FlagThread extends Thread{ private boolean flag = false; @Override public void run() { try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } flag = true; System.out.println("flag : " + flag); } public boolean isFlag() { return flag; } } public class TestVolatile { public static void main(String[] args) { FlagThread flagThread = new FlagThread(); flagThread.start(); while (true){ if (flagThread.isFlag()){ System.out.println("------------------------"); break; } } } }
程序輸出:(main線程結束不了)ide
問題分析:性能
在操做共享數據的時候,系統會將共享數據放置在主存中。優化
流程分析:.net
步驟一:線程
線程1:讀取到的主存數據,flag = true;code
main線程:讀取到的主存數據,flag = true;
步驟二:
線程1:修改自身線程緩存空間的值,令flag = false;並將其更新到主存中,此時主存中的flag = false.
可是,main線程中的值,仍然是,flag = false,並一直循環下去。
所以,main線程始終在循環中,沒法檢測到falg已經變化的值。
volatile能夠禁止進行指令重排。
指令重排是指處理器爲了提升程序運行效率,可能會對輸入代碼進行優化,它不保證各個語句的執行順序同代碼中的順序一致,可是它會保證程序最終執行結果和代碼順序執行的結果是一致的。指令重排序不會影響單個線程的執行,可是會影響到線程併發執行的正確性。
程序執行到volatile修飾變量的讀操做或者寫操做時,在其前面的操做確定已經完成,且結果已經對後面的操做可見,在其後面的操做確定尚未進行。
volatile的使用舉例
//線程1: context = loadContext(); //語句1 context初始化操做 inited = true; //語句2 //線程2: while(!inited ){ sleep() } doSomethingwithconfig(context);
由於指令重排序,有可能語句2會在語句1以前執行,可能致使context還沒被初始化,而線程2中,
// 此處判斷爲false,則不會進入循環 while(!inited ){ sleep() }
於是,線程2使用未初始化的context去進行操做,致使程序出錯。
這裏若是用volatile關鍵字對inited變量進行修飾,就不會出現這種問題了,這是由於volatile禁止指令重排:程序執行到volatile修飾變量的讀操做或者寫操做時,在其前面的操做確定已經完成,且結果已經對後面的操做可見,在其後面的操做確定尚未進行。
synchronized可做用於一段代碼或方法,既能夠保證可見性,又可以保證原子性。
可見性體如今:經過synchronized或者Lock能保證同一時刻只有一個線程獲取鎖而後執行同步代碼,而且在釋放鎖以前會將對變量的修改刷新到主存中。
原子性表如今:要麼不執行,要麼執行到底。
實例講解:必須使用synchronized而不能使用volatile的場景
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); } }
程序分析:
結果:例子中用new了10個線程,分別去調用1000次increase()方法,每次運行結果都不一致,都是一個小於10000的數字。
問題分析:自增操做不是原子操做,volatile 是不能保證原子性的。回到文章一開始的例子,使用volatile修飾int型變量i,多個線程同時進行i++操做。好比有兩個線程A和B對volatile修飾的i進行i++操做,i的初始值是0,A線程執行i++時剛讀取了i的值0,就切換到B線程了,B線程(從內存中)讀取i的值也爲0,而後就切換到A線程繼續執行i++操做,完成後i就爲1了,接着切換到B線程,由於以前已經讀取過了,因此繼續執行i++操做,最後的結果i就爲1了。同理能夠解釋爲何每次運行結果都是小於10000的數字。
可是使用synchronized對部分代碼進行以下修改,就能保證同一時刻只有一個線程獲取鎖而後執行同步代碼。運行結果必然是10000。
public int inc = 0; public synchronized void increase() { inc++; }
內存可見性
在Java中,咱們都知道關鍵字synchronized能夠用於實現線程間的互斥,但咱們卻經常忘記了它還有另一個做用,那就是確保變量在內存的可見性 - 即當讀寫兩個線程同時訪問同一個變量時,synchronized用於確保寫線程更新變量後,讀線程再訪問該 變量時能夠讀取到該變量最新的值。
即當ThreadA釋放鎖M時,它所寫過的變量(好比,x和y,存在它工做內存中的)都會同步到主存中,而當ThreadB在申請同一個鎖M時,ThreadB的工做內存會被設置爲無效,而後ThreadB會從新從主存中加載它要訪問的變量到它的工做內存中(這時x=1,y=1,是ThreadA中修改過的最新的值)。經過這樣的方式來實現ThreadA到ThreadB的線程間的通訊。
//線程A,B共同訪問的代碼 Object lock = new Object(); int a=0; int b=0; int c=0; //線程A,調用以下代碼 synchronized(lock){ a=1; //1 b=2; //2 } //3 c=3; //4 //線程B,調用以下代碼 synchronized(lock){ //5 System.out.println(a); //6 System.out.println(b); //7 System.out.println(c); //8 }
咱們假設線程A先運行,分別給a,b,c三個變量進行賦值(注:變量a,b的賦值是在同步語句塊中進行的),而後線程B再運行,分別讀取出這三個變量的值並打印出來。那麼線程B打印出來的變量a,b,c的值分別是多少?
輸出結果:
線程B裏,打印的a,b確定是1和2. 可是,訪問的到c變量有可能仍是0,而不是3.
(1)從而咱們能夠看出volatile雖然具備可見性可是並不能保證原子性。
(2)性能方面,synchronized關鍵字是防止多個線程同時執行一段代碼,就會影響程序執行效率,而volatile關鍵字在某些狀況下性能要優於synchronized。
可是要注意volatile關鍵字是沒法替代synchronized關鍵字的,由於volatile關鍵字沒法保證操做的原子性。