在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);
}
(0)

相关推荐

  • iOS 9 更新之Safari广告拦截器(Content Blocker)开发教程

    下面通过图文并茂的方式给大家分享下这方面的知识,具体内容如下. 相对于谷歌对广告拦截的禁止,苹果与之态度截然相反,继Mac版Safari加入广告拦截工具之后,即将到来的iOS9对Safari也引入了内容拦截插件-Content Blocker,并且开发者可以使用最新的Xcode7开发以及使用iOS 9模拟器进行调试,下面,笔者将用一个简单的实例进行讲解如何开发Content Blocker. 在使用Google搜索时,排行靠前面的几条永远都是广告项,比如搜索"iPhone6",得到的结

  • iOS利用Block逆向传值的方式详解

    前言 在iOS通过代理逆向传值的方式详解一文中,分析了如何利用代理模式来逆向传值,其实还有一些其他的方式,如通知.Block等,相比较代理,我个人认为反而要简单些,但是需要处理好细节问题,如Block循环引用.还是用前文的案例,本次使用Block来实现,Block的基本知识本文不再赘述. 一.书写规范 Block传值,需要注意的是,谁传值就需要定义Block,捕获方仅仅需要传递Block给传值方,并处理捕获的值. 传值方 1.定义Block用于传值 2.声明一个上述Block属性,这个属性的具体

  • IOS获取各种文件目录路径的方法

    iphone沙箱模型有四个文件夹,分别是什么,永久数据存储一般放在什么位置,得到模拟器的路径的简单方式是什么. documents,tmp,app,Library. (NSHomeDirectory()), 手动保存的文件在documents文件里 Nsuserdefaults保存的文件在tmp文件夹里 1.Documents 目录:您应该将所有de应用程序数据文件写入到这个目录下.这个目录用于存储用户数据或其它应该定期备份的信息. 2.AppName.app 目录:这是应用程序的程序包目录,包

  • iOS中Block的回调使用和解析详解

    Block 回调实现 先跟着我实现最简单的 Block 回调传参的使用,如果你能举一反三,基本上可以满足了 OC 中的开发需求.已经实现的同学可以跳到下一节. 首先解释一下我们例子要实现什么功能(其实是烂大街又最形象的例子): 有两个视图控制器 A 和 B,现在点击 A 上的按钮跳转到视图 B ,并在 B 中的textfield 输入字符串,点击 B 中的跳转按钮跳转回 A ,并将之前输入的字符串 显示在 A 中的 label 上.也就是说 A 视图中需要回调 B 视图中的数据. 想不明白的同学

  • 如何用IOS调用WebService(SOAP接口)

    在一次项目开发过程中,用到IOS调用WebService接口,所以抽个空把这方面的内容给大家整理出来,分享给大家. 方法一:使用WSDL2ObjC工具,将接口转成OC类. 1.在浏览器中输入webService接口地址(Safari不可用,我用的是Firefox),如:http://xxx.xxx.asmx, 地址后面添加上.wsdl成http://xxx.xxx.asmx.wsdl打开. 2.将页面另存为wsdl文件,保存的时候后缀加上.wsdl,保存成如xxxxService.asmx.xm

  • Java 高并发三:Java内存模型和线程安全详解

    网上很多资料在描述Java内存模型的时候,都会介绍有一个主存,然后每个工作线程有自己的工作内存.数据在主存中会有一份,在工作内存中也有一份.工作内存和主存之间会有各种原子操作去进行同步. 下图来源于这篇Blog 但是由于Java版本的不断演变,内存模型也进行了改变.本文只讲述Java内存模型的一些特性,无论是新的内存模型还是旧的内存模型,在明白了这些特性以后,看起来也会更加清晰. 1. 原子性 原子性是指一个操作是不可中断的.即使是在多个线程一起执行的时候,一个操作一旦开始,就不会被其它线程干扰

  • 浅析Java内存模型与垃圾回收

    1.Java内存模型 Java虚拟机在执行程序时把它管理的内存分为若干数据区域,这些数据区域分布情况如下图所示: 程序计数器:一块较小内存区域,指向当前所执行的字节码.如果线程正在执行一个Java方法,这个计数器记录正在执行的虚拟机字节码指令的地址,如果执行的是Native方法,这个计算器值为空. Java虚拟机栈:线程私有的,其生命周期和线程一致,每个方法执行时都会创建一个栈帧用于存储局部变量表.操作数栈.动态链接.方法出口等信息. 本地方法栈:与虚拟机栈功能类似,只不过虚拟机栈为虚拟机执行J

  • iOS通过逆向理解Block的内存模型

    前言 正常情况下,通过分析界面以及 class-dump 出来头文件就能对某个功能的实现猜个八九不离十.但是 Block 这种特殊的类型在头文件中是看不出它的声明的,一些有 Block 回调的方法名 dump 出来是类似这样的: - (void)FM_GetSubscribeList:(long long)arg1 pageSize:(long long)arg2 callBack:(CDUnknownBlockType)arg3; 因为这种回调看不到它的方法签名,我们无法知道这个 Block

  • iOS开发中WebView的基本使用方法简介

    1.使用UIWebView加载网页 运行XCode 4.3,新建一个Single View Application,命名为WebViewDemo. 2.加载WebView 在ViewController.h添加WebView成员变量和在ViewController.m添加实现 复制代码 代码如下: #import <UIKit/UIKit.h> @interface ViewController : UIViewController {     UIWebView *webView; } @e

  • iOS通过block在两个页面间传值的方法

    一.功能需求 在第一个页面中有一个button和一个label,label上默认显示"哈哈",点击button进入第二个页面.在第二个页面有一个UITextField和一个button2,点击button2回到第一个页面,但同时第一个页面的label上显示的文字修改为刚刚在UITextField写进去的文字. 二.先定义block 在要传值得那个页面定义含有block参数的方法,即在第二个页面的.h文件中定义: 重新定义:typedef void (^ReturnTextBlock)(

随机推荐