【问题标题】:Brian Goetz's improper publicationBrian Goetz 的不当发布
【发布时间】:2014-12-28 06:59:17
【问题描述】:

问题已发布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?

【问题讨论】:

  • @SotiriosDelimanolis作者在书中所说的是两个线程可能会看到不同状态的Holder对象,所以在一个线程的Object类中n可能会被初始化为0。
  • 为什么要推迟t2的启动?
  • 该代码的历史原因。我也曾经循环直到 t2 的最大整数,而不是寻找 t1.isAlive()。因为 t2 的执行速度非常快,所以它可以在 t1 没有做任何有用的事情之前完成。但好吧,现在它不再需要了。
  • 我回来是因为我看到了this post
  • 很好的例子@grape_mao(谢谢),我也会试试那个例子。想知道我是否可以将其仅适应一个变量而不是两个变量,这肯定也会产生影响。

标签: java concurrency thread-safety


【解决方案1】:

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

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

不,您的测试没有为编写无法以这种特定方式正确同步的代码提供任何理由。您无法从这些观察中进行概括。

【讨论】:

  • 任何关于会触发 AssertionError 的程序 + VM(供应商 + 版本)组合的想法?除非我看到它发生,否则我不会相信这种行为,尤其是在涉及构造函数的情况下。我假设 VM 供应商会确保交付的对象具有不变量,并且在构造后对所有线程可见。也许两周后我得在 Devoxx Belgium 问问 Brian 本人。
  • 这个特定的例子是故意简单的,用于演示目的。没有真正的 VM 可能在 Holder.assertSanity() 的同一次执行中从主内存读取两次 n,这对于触发 AssertionError 是必要的,但它可以。根据 JMM,所有默认值的分配(在概念上)发生在程序的最开始,因此您甚至不需要对 n 的两次读取进行任何指令重新排序来查看不同的值。依靠虚拟机来保护您免受一般不当发布的影响是非常不明智的。
  • @Juru,就其价值而言,this 看起来像是一个真正不当发布的例子。
【解决方案2】:

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

问题在于,一旦开始处理您的代码,每个优化器都会将您的代码中的两次读取强制为一个,因此在此之后您将永远不会得到AssertionError,即使该单一读取的计算结果为默认值。

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

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

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

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2013-04-13
    • 1970-01-01
    • 2015-01-19
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多