併發-4-volatile

Coding挖掘機發表於2018-10-17

記憶體模型的相關概念:

程式的執行過程為:主存->複製到cache(CPU的快取記憶體)->CPU->重新整理cache->回寫到主存中 共享變數:被多個執行緒同時訪問的變數 特殊情況:主存中i=0,程式A和B分別讀i=0至核心1和核心2(多核CPU)的cache,進行i=i+1,則對這個共享變數雖然操作了兩次,但是最後寫回主存還是i=1。具體過程如下:

    |-------主存-------|----核心1的cache----|----核心2的cache----|

    |-------i=0-------|-----i=0-----------|------i=0-----------|

    |-------i=0-------|-----i=1-----------|------i=1-----------|

    |-------i=1-------|-----i=1-----------|------i=1-----------|
複製程式碼

這就是著名的快取一致性問題 問題的解決方案MESI協議:當CPU寫資料時,如果發現這個變數是共享變數,則通知其他的CPU設定該共享變數的快取行為無效,當其他CPU需要讀取這個變數時,發現自己的快取中快取該變數的快取行是無效的,它就會從記憶體中讀取

原子性:

要麼全部執行,要麼全部不執行,不會被打斷。
eg.對一個32位的int型變數賦值:
eg.i=3分為兩步:1.對低16位賦值。2.對高16位賦值。這時就必須要原子性操作,要麼全部成功,要麼全部不成功
複製程式碼

可見性:

多個執行緒訪問同一個變數的時候,一個執行緒修改了值,另一個執行緒能立即看到修改的值
eg.執行緒1:int i = 0;    i=10,    執行緒2:j=i。
CPU1執行執行緒1,CPU2執行執行緒2
當執行緒1將i=0讀到cache中並且設定為10(執行緒修改了值),但是未寫入主存,執行緒2執行j=i時還是取出主存中i=0的值(未看到最新的值)
複製程式碼

有序性:

以下指令可能會發生指令重排序

語句1:int a=10;

語句2:int r=2;

語句3:a=a+3;

語句4:r=a*a;

考慮資料依賴性之後的指令執行順序可能是2->1->3->4
複製程式碼

多執行緒下的有序性問題:

執行緒1:(語句1)context = loadContext();   

      (語句2)intited=true;

執行緒2:(語句1)while(!inited){

           sleep()

       }

      (語句2)doSomethingwithconfig(context)

因為執行緒1的兩個語句之間沒有依賴,所以可能發生不恰當的指令重排列:如下:

執行緒1先執行了語句2,導致執行緒2的intied誤認為初始化完成,執行緒2跳過語句1,進行了語句2的doSomethingwithconfig(context)工作

由此可見,指令重排列不會影響當個執行緒的執行,但會影響到程式併發的執行
複製程式碼

java記憶體模型:

在JVM規範中,試圖定義了一種Java記憶體模型,(java memeory model,JMM)來保證各個平臺下的記憶體訪問差異,以實現個平臺下的JVM都能達到一致的訪問記憶體的效果,但是JVM沒有限制CPU或者暫存器,更沒有禁止指令重排列,所以也會存在一致性問題。

Java記憶體模型規定所有的變數都是存在主存當中(類似於實體記憶體),每個執行緒都有自己的工作記憶體(類似於快取記憶體)。執行緒對變數的所有操作都必須在工作記憶體中進行,而不能直接對主存進行操作。並且每個執行緒不能訪問其他執行緒的工作記憶體。

原子性:java記憶體模型只對基本資料型別的讀取和賦值(int i = 0)保證了原子性,剩下的需要由synchronized和Lock來實現

可見性:volatile修飾的變數,一旦發生修改,就會更新主存,synchronized和Lock一樣可以保證可見性

synchronized和Lock可以保證在釋放鎖之前將會對變數的修改重新整理到主存中

案例分析:

volatile的意義:

    1.保證了不同執行緒對這個變數進行操作的可見性    

    2.禁止指令重排序
複製程式碼
//執行緒1
boolean isStop = false;
while(!stop){
   doSomething()
}
//執行緒2
stop = true;
複製程式碼
每個執行緒都有自己的工作記憶體,每個執行緒對變數的操作都要在工作記憶體中進行,不能直接對主存進行操作,執行緒之間的工作記憶體相互隔離

執行緒1將isStop讀取到自己的工作記憶體,如果執行緒2在自己的工作記憶體中對isStop進行了修改,但是執行緒1還是沒有進行重新整理,所以會一直執行下去

對執行緒1的isStop加上volatile之後就保證了可見性:

1.當執行緒2修改isStop時,會做兩件事,一就是對isStop立即更新到主存,二就是置執行緒1的快取行為無效

2.當執行緒1再次讀取isStop時,發現自己的快取行無效,就會去讀主存最新的值

volatile可以保證共享變數的可見性
複製程式碼

案例分析2:

public class ThreadVolatile {
    public volatile int inc = 0;

    public void increase() {
        inc++;
    }

    public static void main(String[] args) {
        final ThreadVolatile test = new ThreadVolatile();

        for (int i = 0; i < 1000; i++) {
            new Thread(() -> {
                for (int j = 0; j < 1000; j++) {
                    test.increase();
                }
            }).start();
        }

        while (Thread.activeCount() > 1) {
            //保證前面的執行緒都執行完
            Thread.yield();
        }
        out.println("inc=" + test.inc);
    }
}
複製程式碼

輸出:

inc=999452
複製程式碼
因為自增操作不是原子性的,所以雖然保證了可見性,但還是不夠,每次操作的數都會小於10000

eg.假設i=10此時,執行緒1取出i=10,還未進行(i=i+1,寫入主存)這兩個操作時就阻塞了

   執行緒2取出i=10,完成了i=i+1,寫入了主存i=11

   執行緒1執行i=i+1,寫入主存i=11;

volatile不可以保證原子性

使用AtomicInteger,是JDK中新增的一種利用CAS鎖原理實現的基本資料型別的原子操作

參考下面的程式碼:
複製程式碼
public class ThreadVolatile {
    public AtomicInteger anInt = new AtomicInteger(0);

    public void increase() {
        anInt.incrementAndGet();
    }

    public static void main(String[] args) {
        final ThreadVolatile test = new ThreadVolatile();

        for (int i = 0; i < 1000; i++) {
            new Thread(() -> {
                for (int j = 0; j < 1000; j++) {
                    test.increase();
                }
            }).start();
        }

        while (Thread.activeCount() > 1) {
            //保證前面的執行緒都執行完
            Thread.yield();
        }
        out.println("inc=" + test.anInt.get());
    }
}
複製程式碼

輸出:

inc=1000000
複製程式碼

有序性:

因為volatile不可以保證變數的原子性,但是可以保證變數的可見性,那麼可以部分保證有序性,如下:
複製程式碼
//執行緒1:
context = loadContext();   //語句1
volatile inited = true;             //語句2

//執行緒2:
while(!inited ){
  sleep()
}
doSomethingwithconfig(context);
複製程式碼
這裡如果用volatile關鍵字對inited變數進行修飾,就不會出現這種問題了,因為當執行到語句2時,必定能保證context已經初始化完畢。
複製程式碼

使用場景:

具備條件:

1.對變數的寫操作不依賴於當前值

2.該變數沒有包含在其他變數的不變式中

也就是說,必須保證操作是原子性操作(i++就不可以),才能保證volatile關鍵字的程式在併發的時候能夠正確執行

例如: 狀態標記量:

volatile boolean flag = false;
while(!flag){
    doSomething()
}
public void setFlag(){
    flag = true
}
複製程式碼

//使用於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;
    }
}
複製程式碼

相關文章