开源中国

我们不支持 IE 10 及以下版本浏览器

It appears you’re using an unsupported browser

为了获得更好的浏览体验,我们强烈建议您使用较新版本的 Chrome、 Firefox、 Safari 等,或者升级到最新版本的IE浏览器。 如果您使用的是 IE 11 或以上版本,请关闭“兼容性视图”。
并发酷刑: 在 Java 内存模型中测试代码 - 技术翻译 - 开源中国社区

并发酷刑: 在 Java 内存模型中测试代码 【已翻译100%】

oschina 推荐于 4年前 (共 7 段, 翻译完成于 03-17) 评论 0
收藏  
2
推荐标签: Java 待读

不知道你是否这么觉得,而对于我来说最烦恼的bug就是处理并发问题。除非你已经掌握了利用其他工具摒弃你现有的工具. 但是,这里还有另外一个例子,因为我们已经在这里讲述了很多关于Java的知识,消灭工具并非那么简单

通常我们需要掌握很多技巧才能很好的了解并发问题,同时,也需要一些敏捷调试的能力和修复这些bug的智慧。事实上,其中一个最好的修复这类型bug的方法,就是深入了解你的Java程序内部到底做了些什么。在这篇文章当中,我们利用一个简单的并发程序例子,来证明即使在这么简单的例子当中,程序运行的结果仍然是难于预料。幸好,在JVM生态系统中,有一些并发测试框架,叫做JCStress,通过这个框架,我们可以很好的预测Java程序的结果,同时可以直观的看到,到底程序做了什么事情。

enixyu
 翻译得不错哦!

深入探讨问题

最近,我对一篇描述的Java内存模型,也就是JMM,是如何工作的文章:extremely insightful article,而感到迷惑。对于外行人士, JMM, 参考Java Language Specification第17.4章节,是关于正式的解释了JVM是如何处理内存访问,还有,它回答了一个紧要的问题:

在给定的时间点上,程序的一个读取指令会返回什么结果?

如果你可以读懂俄文,我推荐一个篇由Aleksei Shipilev写的一篇文章,“Pragmatics of Java Memory Model”。这篇文章讲述,JMM是什么东西,为什么它那么重要,还有为什么并发编程那么复杂。另外,毫无疑问,你可以参考其他关于这个主题的资料:

但是,现在,我不想花时间去深入JMM,而是想通过一个例子,介绍它的特点和重要性。


enixyu
 翻译得不错哦!

猜测结果

起初,我从Anton Arhipov那里发现了这个例子,他是JRebel的产品经理,擅长于解决编程语言中遇到的奇怪问题,还有Java和JVM。

让我们来看看这段代码:

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对象的值,然后输出结果。

如果你能回答上面的两个问题,恭喜你,你真的好厉害。跟我发推特@shelajev让我看看你的厉害哦亲。

接下来,我们需要构造一些测试用例来检查这些行为。显然,其中一个只能运行该例子,然后观察结果,回答上面的问题,可是,回答第二个关于允许输出的结果,需要些技巧。

enixyu
 翻译得不错哦!

熟能生巧

幸运的是,我们可以使用工具。 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();
  }
}
huhaonan
 翻译得不错哦!

现在在运行这个测试的时候,控制会去尝试各种花样以求获取驱动这些动作的因素的所有可能组合: 并行的或者非并行的, 有和无负载检测的, 还有一行中进行许多许多次, 因此所有可能的结果都会被记录到.

当你想知道你的并行代码是如何运作的时候,这是比靠你自己去挖空心思想出所有细节更胜一筹的办法.

此外,为了能利用到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"

而我们所得到的结果是一份优雅的报告.

JCStress result report page

LeoXu
 翻译得不错哦!

现在很清楚的是,我们不仅可以得到预期的结果,即两个线程都已经设置了它们的位,也遇到了一个竞争条件,一个线程将覆盖另一个线程的结果。

即使你看到发生了这种事情,也一定要有“山人自有妙计”的淡定心态,不是吗?

顺便说一下,如果你在思考如何修改这个代码,答案是仔细阅读 Javadoc 中的 BitSet 类,并意识到那并非是线程安全的,需要外部同步。这可以很容易地通过增加同步块相关设定值来实现。

synchronized (bs) {
  bs.set(1);
}
赵亮-碧海情天
 翻译得不错哦!

总结

并发是非常复杂的话题并且对并发程序的推理需要很多经验和技巧。幸运的是,Java生态系统已经非常强大健壮而且有很多有用的并发工具。

顺便说一句,如果你有自己的方式来解决并发问题,请在下面的评论中分享下,或者在我的 Twitter上联系我,很乐意听到你的意见。

在这篇文章中我们看了一个简单的关于异步访问BitSet的谜题,但是上面的方法同样适用于所有的Java并发谜题。 你可能偶然发现许多文档中描述的极端案例,但是谁读过呢,对吧?

因此小心翼翼的阅读Javadocs! 或者,你真的很喜欢冒险,看看 Serkan &Ouml;zal’s 关于Java中对象布局,平面图和不安全的 文章 吧。

麦壳原野
 翻译得不错哦!
本文中的所有译文仅用于学习和交流目的,转载请务必注明文章译者、出处、和本文链接
我们的翻译工作遵照 CC 协议,如果我们的工作有侵犯到您的权益,请及时联系我们
评论(0)
Ctrl/CMD+Enter

暂无网友评论
顶部