1. 程式人生 > >併發程式設計三要素:原子性,有序性,可見性

併發程式設計三要素:原子性,有序性,可見性

併發程式設計三要素

  1. 原子性:一個不可再被分割的顆粒。原子性指的是一個或多個操作要麼全部執行成功要麼全部執行失敗。
  2. 有序性: 程式執行的順序按照程式碼的先後順序執行。(處理器可能會對指令進行重排序)
  3. 可見性: 一個縣城對共享變數的修改,另一個執行緒能夠立刻看到。

一、原子性

執行緒切換會帶來原子性的問題

int i = 1; // 原子操作
i++; // 非原子操作,從主記憶體讀取 i 到執行緒工作記憶體,進行 +1,再把 i 寫到主記憶體。

雖然讀取和寫入都是原子操作,但合起來就不屬於原子操作,我們又叫這種為“複合操作”。

我們可以用synchronized 或 Lock 來把這個複合操作“變成”原子操作。

例子:

//使用synchronized
private synchronized void increase(){
   i++;
 }
//使用Lock
private int i = 0;
 Lock mLock = new ReentrantLock();
 
 private void increase() {
   mLock.lock();
   try {
     i++;
   } finally{
     mLock.unlock();
   }
 }

這樣我們就可以把這個一個方法看做一個整體,一個不可分割的整體。

除此之前,我們還可以用java.util.concurrent.atomic裡的原子變數類,可以確保所有對計數器狀態訪問的操作都是原子的。

例子:

  AtomicInteger mAtomicInteger = new AtomicInteger(0);  
  private void increase(){
    mAtomicInteger.incrementAndGet();
  }

二、可見性

快取導致可見性問題

int v = 0;
// 執行緒 A 執行
v++; 
// 執行緒 B 執行
System.out.print("v=" + v);

即使是在執行完執行緒裡的 i++ 後再執行執行緒 B,執行緒 B 的輸入結果也會有 2 個種情況,一個是 0 和1。

因為 i++ 線上程 A(CPU-1)中做完了運算,並沒有立刻更新到主記憶體當中,而執行緒B(CPU-2)就去主記憶體當中讀取並列印,此時列印的就是 0。

禁用快取能保證可見性,volatile關鍵字可以禁用快取

synchronized和Lock能夠保證可見性。

三、有序性

導致有序性的原因是編譯優化

我們都知道處理器為了擁有更好的運算效率,會自動優化、排序執行我們寫的程式碼,但會確保執行結果不變。

例子:

int a = 0; // 語句 1
int b = 0; // 語句 2
i++; // 語句 3
b++; // 語句 4

這一段程式碼的執行順序很有可能不是按上面的 1、2、3、4 來依次執行,因為 1 和 2 沒有資料依賴,3 和 4 沒有資料依賴, 2、1、4、3 這樣來執行可以嗎?完全沒問題,處理器會自動幫我們排序。

在單執行緒看來並沒有什麼問題,但在多執行緒則很容易出現問題。

再來個例子:

// 執行緒 1
init();
inited = true;
 
// 執行緒 2
while(inited){
    work();
}

init(); 與 inited = true; 並沒有資料的依賴,在單執行緒看來,如果把兩句的程式碼調換好像也不會出現問題。

但此時處於一個多執行緒的環境,而處理器真的把這兩句程式碼重新排序,那問題就出現了,若執行緒 1 先執行 inited = true; 此時,init() 並沒有執行,執行緒 2 就已經開始呼叫 work() 方法,此時很可能造成一些奔潰或其他 BUG 的出現。

synchronized和Lock能確保原子性,能讓多執行緒執行程式碼的時候依次按順序執行,自然就具有有序性。

而volatile關鍵字也可以解決這個問題,volatile 關鍵字可以保證有序性,讓處理器不會把這行程式碼進行優化排序。


**** 碼字不易如果對你有幫助請給個關注****

**** 愛技術愛生活 QQ群: 894109590*