讓我們來看看這段代碼:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
|
import java.util.BitSet; import java.util.concurrent.CountDownLatch; public class AnExample { public static void main(String[] args) throws Exception { BitSet bs = new BitSet(); CountDownLatch latch = new CountDownLatch( 1 ); Thread t1 = new Thread( new Runnable() { public void run() { try { latch.await(); Thread.sleep( 1000 ); } catch (Exception ex) { } bs.set( 1 ); } }); Thread t2 = new Thread( new Runnable() { public void run() { try { latch.await(); Thread.sleep( 1000 ); } catch (Exception e) { } bs.set( 2 ); } }); t1.start(); t2.start(); latch.countDown(); t1.join(); t2.join(); // crucial part here: System.out.println(bs.get( 1 )); System.out.println(bs.get( 2 )); } } |
問題來了,這段代碼輸出的結果是什么呢?它究竟能輸出什么結果,上面的程序即使在崩潰的JVM上,仍然允許打印輸出什么結果呢?
讓我們來看看這個程序做了什么:
- 初始化了一個BitSet對象
- 兩個線程并行運行,分別對第一和第二位的字段值設置為true
- 我們嘗試讓這兩個線程同時運行。
- 讀取BitSet對象的值,然后輸出結果。
接下來,我們需要構造一些測試用例來檢查這些行為。顯然,其中一個只能運行該例子,然后觀察結果,回答上面的問題,可是,回答第二個關于允許輸出的結果,需要些技巧。
熟能生巧
幸運的是,我們可以使用工具。 JCStress 就是一個為了解決這類問題而產生的測試工具。
我們可以很容易地將我們的test case寫成JCStress可以識別的形式。事實上, 它已經為我們準備好了多種可能情況下的接口。我們需要一個例子,在這個例子中,2個線程并發地執行,執行的結果表示為2個布爾值。
我們使用一個Actor2_Arbiter1_Test<BitSet, BooleanResult2>接口, 它將為我們的2個線程提供一些方法塊和一個轉換方法,這個轉換方法將表示BitSet狀態的結果轉換成一對布爾值。我們需要找個 Java 8 JVM 來運行它, 但是現在這已經不是什么問題了.
看下面的實現. 是不是特別簡潔?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
|
public class AnExampleTest implements Actor2_Arbiter1_Test<BitSet, BooleanResult2> { @Override public void actor1(BitSet s, BooleanResult2 r) { s.set( 1 ); } @Override public void actor2(BitSet s, BooleanResult2 r) { s.set( 2 ); } @Override public void arbiter1(BitSet s, BooleanResult2 r) { r.r1 = s.get( 1 ); r.r2 = s.get( 2 ); } @Override public BitSet newState() { return new BitSet(); } @Override public BooleanResult2 newResult() { return new BooleanResult2(); } } |
現在在運行這個測試的時候,控制會去嘗試各種花樣以求獲取驅動這些動作的因素的所有可能組合: 并行的或者非并行的, 有和無負載檢測的, 還有一行中進行許多許多次, 因此所有可能的結果都會被記錄到.
當你想知道你的并行代碼是如何運作的時候,這是比靠你自己去挖空心思想出所有細節更勝一籌的辦法.
此外,為了能利用到JCStress 約束帶來的全面性的便利,我們需要給它提供一個對可能結果的解釋. 要那樣做的話我們就需要使用如下所示的一個簡單的XML文件.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
|
< test name = "org.openjdk.jcstress.tests.custom.AnExampleTest" > < contributed-by >Oleg Shelajev</ contributed-by > < description > Tests if BitSet works well without synchronization. </ description > < case > < match >[true, true]</ match > < expect >ACCEPTABLE</ expect > < description > Seeing all updates intact. </ description > </ case > < case > < match >[true, false]</ match > < expect >ACCEPTABLE_INTERESTING</ expect > < description > T2 overwrites T1 result. </ description > </ case > < case > < match >[false, true]</ match > < expect >ACCEPTABLE_INTERESTING</ expect > < description > T1 overwrites T2 result. </ description > </ case > < unmatched > < expect >FORBIDDEN</ expect > < description > All other cases are unexpected. </ description > </ unmatched > </ test > |
現在,我們已經準備好讓這頭野獸開始咆哮了. 通過使用下面的命令行運行測試.
1
|
java -XX:+UnlockDiagnosticVMOptions -XX:+WhiteBoxAPI -XX:-RestrictContended -jar tests-custom /target/jcstress .jar -t= ".*AnExampleTest" |
而我們所得到的結果是一份優雅的報告.
現在很清楚的是,我們不僅可以得到預期的結果,即兩個線程都已經設置了它們的位,也遇到了一個競爭條件,一個線程將覆蓋另一個線程的結果。
即使你看到發生了這種事情,也一定要有“山人自有妙計”的淡定心態,不是嗎?
順便說一下,如果你在思考如何修改這個代碼,答案是仔細閱讀 Javadoc 中的 BitSet 類,并意識到那并非是線程安全的,需要外部同步。這可以很容易地通過增加同步塊相關設定值來實現。
1
2
3
|
synchronized (bs) { bs.set( 1 ); } |