在Java内存模型中测试并发程序代码
让我们来看看这段代码:
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 来运行它, 但是现在这已经不是什么问题了.
看下面的实现. 是不是特别简洁?
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文件.
<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>
现在,我们已经准备好让这头野兽开始咆哮了. 通过使用下面的命令行运行测试.
java -XX:+UnlockDiagnosticVMOptions -XX:+WhiteBoxAPI -XX:-RestrictContended -jar tests-custom/target/jcstress.jar -t=".*AnExampleTest"
而我们所得到的结果是一份优雅的报告.
现在很清楚的是,我们不仅可以得到预期的结果,即两个线程都已经设置了它们的位,也遇到了一个竞争条件,一个线程将覆盖另一个线程的结果。
即使你看到发生了这种事情,也一定要有“山人自有妙计”的淡定心态,不是吗?
顺便说一下,如果你在思考如何修改这个代码,答案是仔细阅读 Javadoc 中的 BitSet 类,并意识到那并非是线程安全的,需要外部同步。这可以很容易地通过增加同步块相关设定值来实现。
synchronized (bs) { bs.set(1); }