前言
最近一段時間,在看併發集合的原始碼,發現了一個非常有趣的現象。我們都知道併發集合,為了保持對其他執行緒的可見性,通常集合中的方法都會使用CAS、volatile、synchronized、Lock等方式。但是在CopyOnWriteArrayList與ConcurrentHashMap中,對其中的存放資料的陣列的操作卻截然不同。
CopyOnWriteArrayList
在CopyOnWiteArrayList中有一個用volatile修飾的陣列(實際存放資料的資料結構),而獲取CopyOnWiteArrayList中的資料直接是呼叫get(int index)方法。並沒有涉及到更多的保持可見性的操作。
private transient volatile Object[] elements;
final Object[] getArray() {
return elements;
}
public E get(int index) {
return get(getArray(), index);
}
private E get(Object[] a, int index) {
return (E) a[index];
}
複製程式碼
ConcurrentHashMap
而在ConcurrentHashMap同樣是獲取用volatile修飾的陣列中的元素,卻呼叫的getObjectVolatile來獲取陣列中的元素。
transient volatile Node<K,V>[] table;
static final <K,V> Node<K,V> tabAt(Node<K,V>[] tab, int i) {
return (Node<K,V>)U.getObjectVolatile(tab, ((long)i << ASHIFT) + ABASE);
}
複製程式碼
疑惑
我相信大家看到這裡,大家和我都有幾個疑惑。
-
在CopyOnWriteArrayList中為什麼就直接使用角標獲取陣列中的元素,而ConcurrentHashMap卻呼叫的是getObjectVolatile方法呢?
-
假如ConcurrentHashMap通過getObjectVolatile方法獲取陣列中的元素是正確的(也就是說volatile修飾的陣列的引用,陣列的元素對其他執行緒不可見,所以我們要使用getObjectVolatile,來保證資料的可見性),那麼CopyOnWriteArrayList簡簡單單的就直接獲取陣列中的元素,就能保證可見性了,這是什麼神奇的操作?
-
假如CopyOnWriteArrayList的操作是正確的(也就是volatile修飾陣列的時候,其內部陣列的元素也是可見的),那麼對於ConcurrentHashMap中的getObjectVolatile方法是不是存在著多餘呢?
開始實驗
到了這裡問題的根源,其實就是volatile修飾的陣列時,陣列的元素是否對其他執行緒存在著可見性。為了解決這個疑惑,所以我就寫了幾個例子。
例子1
在例子1中我宣告瞭一個volatile陣列arr,然後通過一個執行緒1修改arr[19]=2,線上程2中判斷陣列中的arr[19]是否等於2,如果是則跳出迴圈,並列印。具體例子如下:
public class TestVolatile {
public static volatile long[] arr = new long[20];
public static void main(String[] args) throws Exception {
//執行緒1
new Thread(new Thread(){
@Override
public void run() {
//Thread A
try {
TimeUnit.MILLISECONDS.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
arr[19] = 2;
}
}).start();
//執行緒2
new Thread(new Thread(){
@Override
public void run() {
//Thread B
while (arr[19] != 2) {
}
System.out.println("Jump out of the loop!");
}
}).start();
}
}
複製程式碼
執行結果:Jump out of the loop!
WTF!?
程式居然跳出迴圈了!!!!也就是說volatile修飾的陣列確實可以保證陣列中的元素對其他執行緒的可見性?難度是Java開發人員不同,每個人的理解不同,所以寫出了不同的操作?
那ConcurrentHashMap中的getObjectVolatile方法到底是不是多餘的呢?難道是因為ConcurrentHashMap中儲存的是物件,所以要再次通過volatile判斷?為了驗證這個問題,我又寫了例子2。
例子2
在例子2中,我將陣列修改為Object型別,並建立了Person類,因為Person類(包含名字,年齡)是簡單的Java物件,我就沒有在例子中宣告瞭。具體例子如下:
public class TestVolatile {
public static volatile Object[] arr = new Object[20];
public static void main(String[] args) throws Exception {
new Thread(new Thread() {
@Override
public void run() {
//執行緒1
try {
TimeUnit.MILLISECONDS.sleep(1000);
arr[19] = new Person("xixi",12);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
new Thread(new Thread() {
@Override
public void run() {
//執行緒2
while (true) {
Person p = (Person) arr[19];
if (p!=null&&p.getAge()==12) {
break;
}
}
System.out.println("Jump out of the loop!");
}
}).start();
}
}
複製程式碼
執行結果:Jump out of the loop!
從結果來看,也就是說和陣列中儲存的內容是無關的,不管是基本資料型別,還是物件型別。只要是通過volatile修飾的陣列,陣列中的元素對其他執行緒都是可見的
,這裡為什麼ConcurrentHashMap中的getObjectVolatile方法的原因也不得為知。如果大家知道原因的話,希望在部落格下方發表您的留言,讓大家都知道這個知識點。
例子3
雖然說發現了一個神奇的地方,但是好奇心,又驅使我做了另一個實驗。在例子3中,我並沒有用volatile變數修飾陣列,而是在程式中新增了一個用volatile修飾的變數vol。實際的邏輯和例子1差不多,但是線上程2中我讀取了volatile修飾的變數的值。具體例子如下:
public class TestVolatile {
//注意沒用volatile修飾陣列
public static long[] arr = new long[20];
//這裡用volatile修飾另一個變數
public static volatile int vol = 0;
public static void main(String[] args) throws Exception {
//執行緒1
new Thread(new Thread(){
@Override
public void run() {
try {
TimeUnit.MILLISECONDS.sleep(1000);
arr[19] = 2;
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
//執行緒2
new Thread(new Thread(){
@Override
public void run() {
while (true) {
//讀取vol的值
int i = vol;
if (arr[19] == 2) {
break;
}
}
System.out.println("Jump out of the loop!");
}
}).start();
}
}
複製程式碼
執行結果:Jump out of the loop!
WFT!?程式居然又跳出迴圈了,我並沒有用volatile修飾陣列,也就是說陣列中的元素對其他執行緒並不是可見的。那麼程式居然會跳出迴圈,什麼鬼????
為了解決這個問題,我查閱了相關資料,結果在stackoverflow 找到了我想要的答案。在原文的解答中是這樣解釋的:
If Thread A reads a volatile variable, then all all variables visible to Thread A when reading the volatile variable will also be re-read from main memory.
複製程式碼
簡單翻譯一下,就是當一個執行緒在讀取一個使用volatile修飾的變數的時候,會將該執行緒中所有使用的變數從主記憶體中重新讀取。
那麼也就解釋了在例子3中,執行緒2為什麼要跳出迴圈的原因了。
例子4
但是因為自己手賤,我又寫了另一個例子,這個例子,我真的無法解釋為什麼會跳出迴圈的原因,具體例子如下:
public class TestVolatile {
public static long[] arr = new long[20];
public static boolean flag = false;
public static void main(String[] args) throws Exception {
new Thread(new Thread() {
@Override
public void run() {
//執行緒1
try {
TimeUnit.MILLISECONDS.sleep(1000);
arr[19] = 2;
flag = true;
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
new Thread(new Thread() {
@Override
public void run() {
//執行緒2
while (true) {
System.out.println("flag--->" + flag);
if (arr[19] == 2) {
System.out.println("flag in loop--->" + flag);
break;
}
}
System.out.println("Jump out of the loop!");
}
}).start();
}
}
複製程式碼
輸出結果:
...
flag--->false
flag--->false
flag--->false
flag--->false
flag in loop--->true
Jump out of the loop!
複製程式碼
按照可見性原理,按照邏輯思維,程式是不可能跳出迴圈的。但是執行緒2居然迴圈了一定次數後,居然TM的跳出迴圈了。這到底發生了什麼事情。我再一次的懷疑我自己的人生,懷疑我的編譯器,懷疑我的cpu。
總結
寫這個部落格,完全自己一點點的好奇,但是一點一點的,我發現我把自己圈進去了,有可能這就是技術的魅力吧,因為我本身能力也非常有限,有些例子確實無法解釋,這裡把我的疑問丟擲來,希望和大家一起討論討論,希望得到大家的幫助。一起得到一個正確的答案。