Java併發編程——淺談volatile關鍵字

Java提供了一種稍弱的同步機制,即volatile變量,用來確保將變量的更新操做通知到其餘線程。當把變量聲明爲volatile類型後,編譯器與運行時都會注意到這個變量是共享的,volatile變量不會被緩存在寄存器或者其餘處理器不可見的地方,所以在讀取volatile類型的變量時總會返回最新的值。java

1.volatile關鍵字的兩層語義c++

一旦一個共享變量(類的成員變量、類的靜態成員變量)被volatile修飾以後,那麼就具有了兩層語義:緩存

1)保證了不一樣線程對這個變量進行操做時的可見性,即一個線程修改了某個變量的值,這新值對其餘線程來講是當即可見的。優化

2)禁止進行指令重排序。atom

先看下面一段代碼spa

//線程1
boolean alseep= false;
while(!alseep){
    doSomething();
}
 
//線程2
alseep= true;

 

這段代碼爲什麼有可能致使沒法中斷線程。在前面已經解釋過,每一個線程在運行過程當中都有本身的工做內存,那麼線程1在運行的時候,會將stop變量的值拷貝一份放在本身的工做內存當中。線程

那麼當線程2更改了stop變量的值以後,可是還沒來得及寫入主存當中,線程2轉去作其餘事情了,那麼線程1因爲不知道線程2對stop變量的更改,所以還會一直循環下去。code

若是還不清楚明白,看線程讀取變量的示意圖?排序

好比:線程1修改了變量的值,也刷新到了主存中,可是線程二、線程3在此段時間內讀取仍是工做內存中的,也就是說他們讀取的不是「最新的值」,此時就出現了不一樣的線程持有的公共資源不一樣步的狀況。內存

若是加入volatile修飾,就能夠生效

//線程1
volatile boolean alseep= false;
while(!alseep){
    doSomething();
}
 
//線程2
alseep= true;

 

1:使用volatile關鍵字會強制將修改的值當即寫入主存;

2:使用volatile關鍵字的話,當線程2進行修改時,會致使線程1的工做內存中緩存變量stop的緩存行無效(反映到硬件層的話,就是CPU的L1或者L2緩存中對應的緩存行無效);

3:因爲線程1的工做內存中緩存變量stop的緩存行無效,因此線程1再次讀取變量stop的值時會去主存讀取

若是仍是有點含糊,看下volatile讀取變量的示意圖?

若是在變量前加上volatile關鍵字,能夠確保每一個線程對本地變量的訪問和修改都是直接與主存進行交互。

而不是像上面與工做內存相交互。因此能夠保證每一個線程均可以得到最新的值。

 

2.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);
    }
}

這段程序的輸出結果是多少?也許不少朋友認爲是10000。可是事實上運行它會發現每次運行結果都不一致,都是一個小於10000的數字。

可是要注意,假如線程1對變量進行讀取操做以後,被阻塞了的話,並無對inc值進行修改。而後雖然volatile能保證線程2對變量inc的值讀取是從內存中讀取的,可是線程1沒有進行修改,因此線程2根本就不會看到修改的值。

根源就在這裏,自增操做不是原子性操做,並且volatile也沒法保證對變量的任何操做都是原子性的。

把上面的代碼改爲如下任何一種均可以達到效果:

採用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操做)、以及加法操做(加一個數),減法操做(減一個數)進行了封裝,保證這些操做是原子性操做。volatile 修飾基本類型必需要肯定變量操做是原子操做,但count++並非原子操做,其中有讀,加,寫三個操做。

3.volatile能保證有序性嗎?

volatile關鍵字能禁止指令重排序,因此volatile能在必定程度上保證有序性。

volatile關鍵字禁止指令重排序有兩層意思:

1:當程序執行到volatile變量的讀操做或者寫操做時,在其前面的操做的更改確定所有已經進行,且結果已經對後面的操做可見;在其後面的操做確定尚未進行;

2:在進行指令優化時,不能將在對volatile變量訪問的語句放在其後面執行,也不能把volatile變量後面的語句放到其前面執行。

4.volatile的原理和實現機制

下面這段話摘自《深刻理解Java虛擬機》:

「觀察加入volatile關鍵字和沒有加入volatile關鍵字時所生成的彙編代碼發現,加入volatile關鍵字時,會多出一個lock前綴指令」

lock前綴指令實際上至關於一個內存屏障(也成內存柵欄),內存屏障會提供3個功能:

1:它確保指令重排序時不會把其後面的指令排到內存屏障以前的位置,也不會把前面的指令排到內存屏障的後面;即在執行到內存屏障這句指令時,在它前面的操做已經所有完成;

2:它會強制將對緩存的修改操做當即寫入主存;

3:若是是寫操做,它會致使其餘CPU中對應的緩存行無效。

使用volatile關鍵字的場景

不要將volatile用在getAndOperate場合(這種場合不原子,須要再加鎖),僅僅set或者get的場景是適合volatile的。加鎖機制既能夠確保可見性又能夠確保原子性,而volatile變量只可確保可見性。一般來講,當且僅當知足如下全部條件時。才應該可使用volatile變量:

1:對變量的寫入操做不依賴變量的當前值,或者你能確保只有單個線程更新變量的值。

2:該變量不會與其餘狀態變量一塊兒歸入不變性的條件中。

3:在訪問變量時不須要加鎖。

相關文章
相關標籤/搜索