線程同步Volatile與Synchronized(一)

volatile

1、volatile修飾的變量具備內存可見性

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已經變化的值。

2、volatile禁止指令重排

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

3、synchronized

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.

4、總結

(1)從而咱們能夠看出volatile雖然具備可見性可是並不能保證原子性。

(2)性能方面,synchronized關鍵字是防止多個線程同時執行一段代碼,就會影響程序執行效率,而volatile關鍵字在某些狀況下性能要優於synchronized。

可是要注意volatile關鍵字是沒法替代synchronized關鍵字的,由於volatile關鍵字沒法保證操做的原子性。

參考資料:

Java併發——線程同步Volatile與Synchronized詳解

相關文章
相關標籤/搜索