Java併發之原子性、有序性、可見性

小白程式之路發表於2019-03-05
原子性

​ 原子性指的是一個或者多個操作在 CPU 執行的過程中不被中斷的特性

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

Java 併發程式都是基於多執行緒的,作業系統為了充分利用CPU的資源,將CPU分成若干個時間片,在多執行緒環境下,執行緒會被作業系統排程進行任務切換。

image-20190304155302961

為了直觀的瞭解什麼是原子性,我們看下下面哪些操作是原子性操作

int count = 0; //1
count++;       //2
int a = count; //3
複製程式碼

上面展示語句中,除了語句1是原子操作,其它兩個語句都不是原子性操作,下面我們來分析一下語句2

其實語句2在執行的時候,包含三個指令操作

  • 指令 1:首先,需要把變數 count 從記憶體載入到 CPU的暫存器
  • 指令 2:之後,在暫存器中執行 +1 操作;
  • 指令 3:最後,將結果寫入記憶體

對於上面的三條指令來說,如果執行緒 A 在指令 1 執行完後做執行緒切換,執行緒 A 和執行緒 B 按照下圖的序列執行,那麼我們會發現兩個執行緒都執行了 count+=1 的操作,但是得到的結果不是我們期望的 2,而是 1。

image-20190304163607329

作業系統做任務切換,可以發生在任何一條CPU 指令執行完

有序性

​ 有序性指的是程式按照程式碼的先後順序執行

**編譯優化 **帶來的有序性問題

為了效能優化,編譯器和處理器會進行指令重排序,有時候會改變程式中語句的先後順序,比如程式:

a = 5;     //1
b = 20;    //2
c = a + b; //3
複製程式碼

編譯器優化後可能變成

b = 20;    //1
a = 5;     //2
c = a + b; //3
複製程式碼

在這個例子中,編譯器調整了語句的順序,但是不影響程式的最終結果

synchronized(具有有序性、原子性、可見性)表示鎖在同一時刻只能由一個執行緒進行獲取,當鎖被佔用後,其他執行緒只能等待。

在單例模式的實現上有一種雙重檢驗鎖定的方式(Double-checked Locking)

public class Singleton {
  static Singleton instance;
  static Singleton getInstance(){
    if (instance == null) {
      synchronized(Singleton.class) {
        if (instance == null)
          instance = new Singleton();
        }
    }
    return instance;
  }
}
複製程式碼

我們先看 instance = new Singleton() 的未被編譯器優化的操作

  • 指令 1:分配一塊記憶體 M;

  • 指令 2:在記憶體 M 上初始化 Singleton 物件;

  • 指令 3:然後 M 的地址賦值給 instance 變數。

編譯器優化後的操作指令

  • 指令 1:分配一塊記憶體 M;

  • 指令 2:將 M 的地址賦值給 instance 變數;

  • 指令 3:然後在記憶體 M 上初始化 Singleton 物件。

現在有A,B兩個執行緒,我們假設執行緒A先執行getInstance()方法,當執行編譯器優化後的操作指令2時(此時候未完成物件的初始化),這時候發生了執行緒切換,那麼執行緒B進入,剛好執行到第一次判斷 instance==null會發現instance不等於null了,所以直接返回instance,而此時的 instance 是沒有初始化過的。

image-20190304173844373

現行的比較通用的做法就是採用靜態內部類的方式來實現

public class SingletonDemo {
    private SingletonDemo() {
    }
    private static class SingletonDemoHandler{
        private static SingletonDemo instance = new SingletonDemo();
    }
    public static SingletonDemo getInstance() {
        return SingletonDemoHandler.instance;
    }
}
複製程式碼
可見性

​ 可見性指的是當一個執行緒修改了共享變數後,其他執行緒能夠立即得知這個修改

快取 導致的可見性問題

首先我們來看一下Java記憶體模型(JMM)

image-20190304183341997

  • 我們定義的所有變數都儲存在主記憶體
  • 每個執行緒都有自己獨立的工作記憶體,裡面儲存該執行緒使用到的變數的副本(主記憶體中該變數的一份拷貝)
  • 執行緒對共享變數所有的操作都必須在自己的工作記憶體中進行,不能直接從主記憶體中讀寫(不能越級)
  • 不同執行緒之間也無法直接訪問其他執行緒的工作記憶體中的變數,執行緒間變數值的傳遞需要通過主記憶體來進行。(同級不能相互訪問)

共享變數可見性的實現原理:

執行緒1對共享變數的修改要被執行緒2及時看到的話,要經過如下步驟:

  1. 把工作記憶體1中更新的變數值重新整理到主記憶體
  2. 把主記憶體中的變數的值更新到工作記憶體2中

可以使用 synchronizedvolatilefinal 來保證可見性

歡迎關注公眾號

image-20190304183341997

相關文章