一、原子性
原子,一個不可再被分割的顆粒。原子性,指的是一個或多個不能再被分割的操作。
int i = 1; // 原子操作
i++; // 非原子操作,從主內存讀取 i 到線程工作內存,進行 +1,再把 i 寫到朱內存。
雖然讀取和寫入都是原子操作,但合起來就不屬于原子操作,我們又叫這種為“復合操作”。
我們可以用synchronized 或 Lock 來把這個復合操作“變成”原子操作。
例子:
1
2
3
|
private synchronized void increase(){ i++; } |
或
1
2
3
4
5
6
7
8
9
10
11
|
private int i = 0 ; Lock mLock = new ReentrantLock(); private void increase() { mLock.lock(); try { i++; } finally { mLock.unlock(); } } |
這樣我們就可以把這個一個方法看做一個整體,一個不可分割的整體。
除此之前,我們還可以用java.util.concurrent.atomic里的原子變量類,可以確保所有對計數器狀態訪問的操作都是原子的。
例子:
1
2
3
4
5
|
AtomicInteger mAtomicInteger = new AtomicInteger( 0 ); private void increase(){ mAtomicInteger.incrementAndGet(); } |
二、可見性
當多線程訪問某一個(同一個)變量時,其中一條線程對此變量作出修改,其他線程可以立刻讀取到最新修改后的變量。
1
2
3
4
5
6
|
int i = 0 ; // 線程 1 執行 i++; // 線程 2 執行 System.out.print( "i=" + i); |
即使是在執行完線程里的 i++ 后再執行線程 2,線程 2 的輸入結果也會有 2 個種情況,一個是 0 和 1。
因為 i++ 在線程 1(CPU1)中做完了運算,并沒有立刻更新到主內存當中,而線程 2(CPU2)就去主內存當中讀取并打印,此時打印的就是 0。
synchronized和Lock能夠保證可見性。
另外volatile關鍵字也可以解決這個問題(下一篇會講到)。
三、有序性
我們都知道處理器為了擁有更好的運算效率,會自動優化、排序執行我們寫的代碼,但會確保執行結果不變。
例子:
1
2
3
4
|
int a = 0 ; // 語句 1 int b = 0 ; // 語句 2 i++; // 語句 3 b++; // 語句 4 |
這一段代碼的執行順序很有可能不是按上面的 1、2、3、4 來依次執行,因為 1 和 2 沒有數據依賴,3 和 4 沒有數據依賴, 2、1、4、3 這樣來執行可以嗎?完全沒問題,處理器會自動幫我們排序。
在單線程看來并沒有什么問題,但在多線程則很容易出現問題。
再來個例子:
1
2
3
4
5
6
7
8
|
// 線程 1 init(); inited = true ; // 線程 2 while (inited){ work(); } |
init(); 與 inited = true; 并沒有數據的依賴,在單線程看來,如果把兩句的代碼調換好像也不會出現問題。
但此時處于一個多線程的環境,而處理器真的把這兩句代碼重新排序,那問題就出現了,若線程 1 先執行 inited = true; 此時,init() 并沒有執行,線程 2 就已經開始調用 work() 方法,此時很可能造成一些奔潰或其他 BUG 的出現。
synchronized和Lock能確保原子性,能讓多線程執行代碼的時候依次按順序執行,自然就具有有序性。
而volatile關鍵字也可以解決這個問題,volatile 關鍵字可以保證有序性,讓處理器不會把這行代碼進行優化排序。
原文鏈接:http://hackeris.me/2017/03/13/concurrent_series_1/