本章主要學習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接口保障可見性。