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介面保障可見性。
以上兩種有序性保障技術會在後續章節中繼續學習。