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

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

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

本章的很多概念可以參考:Java併發11:Java內存模型、指令重排、happens-before原則

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接口保障可見性。
 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章