在对象构造后读取字段过时值

我正在读一本由Brian Goetz撰写的《Java并发实践》一书。第3.5段和第3.5.1段包含我无法理解的陈述。

请考虑以下代码:

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

  public void assertValue() {
    if (value != value) throw new AssertionError("Magic");
  }
}

class HolderContainer {
  // Unsafe publication
  public Holder holder;

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

作者指出:

  1. 在 Java 中,对象构造函数首先将默认值写入所有字段,然后再运行子类构造函数。
  2. 因此,可以将字段默认值视为过时值。
  3. 线程可能会在第一次读取字段时看到过时的值,然后在下次看到更新的值,这就是 assertN 可以抛出 AssertionError 的原因。

因此,根据文本,如果时间不走运,则值= 0;在下一刻值 = 42。

我同意第1点,对象构造函数首先用默认值填充字段。但我不明白第2点和第3点。

让我们更新作者代码并考虑以下示例:

public class Holder {
  int value;

  public Holder(int value) {
    //Sleep to prevent constructor to finish too early
    try {
     Thread.sleep(3000);
    } catch (InterruptedException e) {
     e.printStackTrace();
    }
    this.value = value;
  }

  public void assertValue()  {
    if(value != value) System.out.println("Magic");
  }
}

我添加了 Thread.sleep(3000),以强制线程等待对象完全构造。

public class Tests {

  private HolderContainer hc = new HolderContainer();

  class Initialization implements Runnable {
    public void run() {
      hc.init();
    }
  }

  class Checking implements Runnable {
    public void run() {
      hc.holder.assertValue();
    }
  }

  public void run() {
    new Thread(new Initialization()).start();
    new Thread(new Checking()).start();
  }
}

例如:

  1. 第一个线程初始化持有者对象
  2. 第二个线程调用断言值

主线程运行两个线程:

  1. new Thread(new Initialization()).start();完全构造 Holder 对象需要 3 秒钟
  2. new Thread(new Checking()).start();由于持有者对象仍未构造代码将抛出 NullPointerException

因此,当字段具有默认值时,不可能模拟情况。

我的问题:

  1. 作者对这个并发问题有错吗?
  2. 还是无法模拟字段默认值的行为?

答案 1

我尝试使用以下代码测试问题。

测试:

public class Test {
    public static boolean flag =true;
    public static HolderContainer hc=new HolderContainer();

    public static void main (String args[]){    
        new Thread(new Initialization()).start();
        new Thread(new Checking()).start();
    }
}

class Initialization implements Runnable {
    public void run() {
        while (Test.flag){
            Test.hc=new HolderContainer();
            Test.hc.init();
            }
    }
}

class Checking implements Runnable {
    public void run() {
        try{
            Test.hc.holder.assertValue();
        }
        catch (NullPointerException e) {
        }    
    }
}

持有:

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

    public void assertValue() {
        if (value != value) {
            System.out.println("Magic");
            Test.flag=false;
        }
    }
}

class HolderContainer {
    public Holder holder;
    public void init() {
        holder = new Holder(42);  
    }
}

我从来没有得到程序来评估。我不认为这证明了什么,也没有运行它超过几分钟,但我希望这将是一个设计良好的测试的更好起点,或者至少有助于找出测试中一些可能的缺陷。value!=valuetrue

我试图在 和 之间插入睡眠, 在 和 之间和之后。Test.hc=new HolderContainer();Test.hc.init();public Holder holder;public void init() {public void init() {

我还担心检查值是否为或捕获可能会对时间产生太大影响。nullNullPoiterException

请注意,目前接受的 Java 对象引用不当发布的答案表明,在 x86 体系结构下,此问题可能是不可能的。它也可能依赖于 JVM。


答案 2

您可能正在尝试模拟并发方案,我认为使用几个线程很难模拟并发方案。

您编写的以下测试用例根本不正确,更有可能抛出NullPointerException.

public class Tests {

  private HolderContainer hc = new HolderContainer();

  class Initialization implements Runnable {
    public void run() {
      hc.init();
    }
  }

  class Checking implements Runnable {
    public void run() {
      hc.holder.assertValue();
    }
  }

  public void run() {
    new Thread(new Initialization()).start();  
    new Thread(new Checking()).start(); 
  }
}

如果您的检查线程在初始化之前执行,该怎么办?此外,将休眠放在那里只是意味着执行线程将休眠,并且确实会告诉您此时正在执行的内部原子操作。


推荐