1. 程式人生 > >Java併發12:併發三特性-原子性、可見性和有序性概述及問題示例

Java併發12:併發三特性-原子性、可見性和有序性概述及問題示例

本章主要學習Java併發中的三個特性:原子性、可見性和有序性。

在Java併發程式設計中,如果要保證程式碼的安全性,則必須保證程式碼的原子性、可見性和有序性。

1.原子性(Atomicity)

1.1.原子性定義

原子性:一個或多個操作,要麼全部執行且在執行過程中不被任何因素打斷,要麼全部不執行。

1.2.Java自帶的原子性

在Java中,對基本資料型別的變數讀取賦值操作是原子性操作。

正確理解Java自帶的原子性。下面的變數a、b都是基本資料型別的變數

a = true;//1
a = 5;//2
a = b;//3
a = b + 2;//4
a ++;//5

上面的5個基本資料型別的操作,只有1和2是原子性的。

  • a = true:包含一個操作,1.將true的賦值給a。
  • a = 5:包含一個操作,1.將5的賦值給a。
  • a = b:包含兩個操作,1.讀取b的值;2.將b的值賦值給a。
  • a = b + 2:包含三個操作,1.讀取b的值;2.計算b+2;3.將b+2的計算結果賦值給a。
  • a ++:即a = a + 1,包含三個操作,讀取a的值;2計算a+1;3.將a+1的計算結果賦值給a。

1.3.原子性問題示例

由上面的章節已知,不採取任何的原子性保障措施的自增操作並不是原子性的。
下面的程式碼實現了一個自增器(不是原子性的)。

/**
 * <p>原子性示例:不是原子性</p>
 *
 * @author
hanchao 2018/3/10 14:58 **/
static class Increment { private int count = 1; public void increment() { count++; } public int getCount() { return count; } }

下面的程式碼展示了在多執行緒環境中,呼叫此自增器進行自增操作。

int type = 0;//型別
int num = 50000;//自增次數
int sleepTime = 5000;//等待計算時間
int begin;//開始的值
Increment increment;
//不進行原子性保護的大範圍操作
increment = new Increment(); begin = increment.getCount(); LOGGER.info("Java中普通的自增操作不是原子性操作。"); LOGGER.info("當前執行類:" +increment.getClass().getSimpleName() + ",count的初始值是:" + increment.getCount()); for (int i = 0; i < num; i++) { new Thread(() -> { increment.increment(); }).start(); } //等待足夠長的時間,以便所有的執行緒都能夠執行完 Thread.sleep(sleepTime); LOGGER.info("進過" + num + "次自增,count應該 = " + (begin + num) + ",實際count = " + increment.getCount());

某次執行結果:

2018-03-17 22:52:23 INFO  ConcurrentAtomicityDemo:132 - Java中普通的自增操作不是原子性操作。
2018-03-17 22:52:23 INFO  ConcurrentAtomicityDemo:133 - 當前執行類:Increment,count的初始值是:1
2018-03-17 22:52:33 INFO  ConcurrentAtomicityDemo:141 - 進過50000次自增,count應該 = 50001,實際count = 49999

通過觀察結果,發現程式確實存在原子性問題。

1.4.原子性保障技術

在Java中提供了多種原子性保障措施,這裡主要涉及三種:

  • 通過synchronized關鍵字定義同步程式碼塊或者同步方法保障原子性。
  • 通過Lock介面保障原子性。
  • 通過Atomic型別保障原子性。

以上三種原子性保障技術會在後續章節中繼續學習。

2.可見性(Visibility)

2.1.可見性定義

可見性:當一個執行緒修改了共享變數的值,其他執行緒能夠看到修改的值。

有前面的文章可知,JVM物件變數的修改存在從Heap載入到Heap更新的過程,所以存在可見性問題。

2.2.可見性問題示例

場景說明:

  • 存在兩個執行緒A、執行緒B和一個共享變數stop。
  • 如果stop變數的值是false,則執行緒A會一直執行。如果stop變數的值是true,則執行緒A會停止執行。
  • 執行緒B能夠將共享變數stop的值修改為ture。

程式碼:
首先,定義一個共享變數stop(存在可見性問題):

//普通情況下,多執行緒不能保證可見性
private static boolean stop;

然後,啟動執行緒A和執行緒B:

//普通情況下,多執行緒不能保證可見性
new Thread(() -> {
    System.out.println("Ordinary A is running...");
    while (!stop) ;
    System.out.println("Ordinary A is terminated.");
}).start();
Thread.sleep(10);
new Thread(() -> {
    System.out.println("Ordinary B is running...");
    stop = true;
    System.out.println("Ordinary B is terminated.");
}).start();

執行結果:

Ordinary A is running...
Ordinary B is running...
Ordinary B is terminated.

從結果觀察,發現執行緒B執行結束了,也就是說已經修改了共享變數stop的值。但是執行緒A還在執行,也就是說執行緒A並沒有用接收到stop=true這個修改。

1.4.可見性保障技術

在Java中提供了多種可見性保障措施,這裡主要涉及四種:

  • 通過volatile關鍵字標記記憶體屏障保證可見性。
  • 通過synchronized關鍵字定義同步程式碼塊或者同步方法保障可見性。
  • 通過Lock介面保障可見性。
  • 通過Atomic型別保障可見性。

以上四種可見性保障技術會在後續章節中繼續學習。

3.有序性(orderly)

3.1.有序性定義

有序性:即程式執行的順序按照程式碼的先後順序執行。

有前面的文章可知,JVM存在指令重排,所以存在有序性問題。

在Java中,由於happens-before原則,單執行緒內的程式碼是有序的,可以看做是序列(as-if-serial)執行的。但是在多執行緒環境下,多個執行緒的程式碼是交替的序列執行的,這就產生了有序性問題。

3.2.Java自帶的有序性

在前面的文章可知,Java提供了happens-before原則保證程式基本的有序性,主要規則如下:

  • 執行緒內部規則:在同一個執行緒內,前面操作的執行結果對後面的操作是可見的。
  • 同步規則:如果一個操作x與另一個操作y在同步程式碼塊/方法中,那麼操作x的執行結果對操作y可見。
  • 傳遞規則:如果操作x的執行結果對操作y可見,操作y的執行結果對操作z可見,則操作x的執行結果對操作z可見。
  • 物件鎖規則:如果執行緒1解鎖了物件鎖a,接著執行緒2鎖定了a,那麼,執行緒1解鎖a之前的寫操作的執行結果都對執行緒2可見。
  • volatile變數規則:如果執行緒1寫入了volatile變數v,接著執行緒2讀取了v,那麼,執行緒1寫入v及之前的寫操作的執行結果都對執行緒2可見。
  • 執行緒start原則:如果執行緒t在start()之前進行了一系列操作,接著進行了start()操作,那麼執行緒t在start()之前的所有操作的執行結果對start()之後的所有操作都是可見的。
  • 執行緒join規則:執行緒t1寫入的所有變數,在任意其它執行緒t2呼叫t1.join()成功返回後,都對t2可見。

而有序性問題,都是發生在happens-before原則之外的狀況。

3.3.有序性問題示例

前置說明,其實網上有很多關於有序性的例項,類似如下:

//執行緒1:
context = loadContext();   //語句1
inited = true;             //語句2

//執行緒2:
while(!inited ){
  sleep()
}
doSomethingwithconfig(context);

不過本人通過實際程式設計,並沒有重現這段程式的無序性。

所以為了更方便的理解有序性問題,本人使用了後面的示例,雖然這個示例有些不太匹配。

場景說明:

  • 有兩個執行緒A和執行緒B。
  • 執行緒A對變數x進行加法和減法操作。
  • 執行緒B對變數x進行乘法和出發操作。

程式碼:
這裡的示例只是為了方便得到無序的結果而專門寫到,所以有些奇特。

static String a1 = new String("A : x = x + 1");
static String a2 = new String("A : x = x - 1");
static String b1 = new String("B : x = x * 2");
static String b2 = new String("B : x = x / 2");

//不採取有序性措施,也沒有發生有序性問題.....
LOGGER.info("不採取措施:單執行緒序列,視為有序;多執行緒交叉序列,視為無序。");
new Thread(() -> {
    System.out.println(a1);
    try {
        Thread.sleep(10);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    System.out.println(a2);
}).start();
new Thread(() -> {
    System.out.println(b1);
    System.out.println(b2);
}).start();

執行結果:

2018-03-18 00:16:20 INFO  ConcurrentOrderlyDemo:63 - 不採取措施:單執行緒序列,視為有序;多執行緒交叉序列,視為無序。
A : x = x + 1
B : x = x * 2
B : x = x / 2
A : x = x - 1

通過執行結果發現,多執行緒環境中,程式碼是交替的序列執行的,這樣會導致產生意料之外的結果。

3.4.有序性保障技術

在Java中提供了多種有序性保障措施,這裡主要涉及兩種:

  • 通過synchronized關鍵字定義同步程式碼塊或者同步方法保障可見性。
  • 通過Lock介面保障可見性。

以上兩種有序性保障技術會在後續章節中繼續學習。

參考文獻