Brian Goetz的不正当出版物

时间:2014-10-31 21:09:22

标签: java concurrency thread-safety

问题已发布before,但没有提供有效的实例。所以Brian提到在某些情况下,AssertionError可能出现在以下代码中:

public class Holder {
  private int n;
  public Holder(int n) { this.n = n; }

  public void assertSanity() {
    if (n!=n)
      throw new AssertionError("This statement is false");
  }
}

当持有人不正确地发布时:

class someClass {
    public Holder holder;

    public void initialize() {
        holder = new Holder(42);
    }
}

据我所知,在对象持有者的实例变量对另一个线程可见之前,对holder的引用可见时会发生这种情况。所以我做了以下示例来激发此行为,从而引发具有以下类的AssertionError:

public class Publish {

  public Holder holder;

  public void initialize() {
    holder = new Holder(42);
  }

  public static void main(String[] args) {
    Publish publish = new Publish();
    Thread t1 = new Thread(new Runnable() {
      public void run() {
        for(int i = 0; i < Integer.MAX_VALUE; i++) {
          publish.initialize();
        }
        System.out.println("initialize thread finished");
      }
    });

    Thread t2 = new Thread(new Runnable() {
      public void run() {
        int nullPointerHits = 0;
        int assertionErrors = 0;
        while(t1.isAlive()) {
          try {
            publish.holder.assertSanity();
          } catch(NullPointerException exc) {
            nullPointerHits++;
          } catch(AssertionError err) {
            assertionErrors ++;
          }
        }
        System.out.println("Nullpointerhits: " + nullPointerHits);
        System.out.println("Assertion errors: " + assertionErrors);
      }
    });

    t1.start();
    t2.start();
  }

}

无论我运行多少次代码,都不会发生AssertionError。所以对我来说有几种选择:

  • jvm实现(在我的例子中是Oracle的1.8.0.20)强制执行在构造对象期间设置的不变量对所有线程都可见。
  • 这本书是错的,我怀疑作者是Brian Goetz ...... nuf说
  • 我在上面的代码中做错了

所以我有问题: - 有人成功地引发了这种AssertionError吗?用什么代码呢? - 为什么我的代码不能引发AssertionError?

2 个答案:

答案 0 :(得分:2)

您的程序未正确同步,因为该术语由Java内存模型定义。

但是,这并不意味着任何特定的运行都会显示您正在寻找的断言失败,也不一定意味着永远看到失败。可能是您的特定VM恰好以一种从未暴露同步失败的方式处理该特定程序。或者它可能会导致虽然容易失败,但可能性很小。

不,您的测试没有为编写无法以这种特定方式正确同步的代码提供任何理由。你不能从这些观察中推广出来。

答案 1 :(得分:1)

您正在寻找一种非常罕见的疾病。即使代码读取未初始化的n,它也可能在下次读取时读取相同的默认值,因此您要查找的竞赛需要在这两个相邻读取之间进行更新。

问题在于,一旦开始处理代码,每个优化器都会将代码中的两个读取强制转换为一个,所以在此之后,即使单个读取的值达到默认值,也永远不会得到AssertionError

此外,由于对Publish.holder的访问是不同步的,因此允许优化器只读取其值一次,并在所有后续迭代期间保持不变。因此,优化的第二个线程将始终处理相同的对象,该对象永远不会返回到未初始化状态。更糟糕的是,乐观优化器可能会假设n始终为42,因为您从未将其初始化为此运行时中的其他内容,并且它不会考虑您想要的情况竞争条件。因此,两个循环都可以优化为无操作。

换句话说:如果您的代码在第一次访问时没有失败,那么在后续迭代中发现错误的可能性会急剧下降,可能会降至零。这与你的想法相反,让代码在长循环中运行,希望你最终会遇到错误。

获得数据竞争的最佳机会在于代码的第一次非优化解释执行。但请记住,即使以纯解释模式运行整个测试代码,特定数据竞争的可能性仍然非常低。