1

我正在查看“Java Concurrency in Practice”一书,发现下面引用的陈述真的很难相信(但不幸的是它是有道理的)。

http://www.informit.com/store/java-concurrency-in-practice-9780321349606

只是想弄清楚这 100%

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.");
      }
}

虽然在构造函数中设置的字段值似乎是写入这些字段的第一个值,因此没有“旧”值可以视为陈旧值, 但 Object 构造函数首先在子类构造函数运行之前将默认值写入所有字段. 因此,可以将字段的默认值视为陈旧值

关于上面的粗体声明,

我知道这种行为,但现在很明显,构造函数的这种调用层次结构不能保证是原子的(在由锁保护的单个同步块中调用超级构造函数),但是解决方案是什么?想象一个具有多个级别的类层次结构(即使不推荐,让我们假设它是可能的)。上面的代码片段是我们在大多数项目中每天都能看到的一种原型。

4

4 回答 4

2

你看错书了。它明确地说:

这里的问题不是 Holder 类本身,而是 Holder 没有正确发布。

所以上面的构造如果没问题。不好的是不正确地将这样的对象发布到其他线程。书中详细解释了这一点。

于 2013-10-04T11:39:38.930 回答
1

当创建一个新对象时,事情是按顺序发生的。我不知道确切的顺序,但它类似于:分配空间并将其初始化为零,然后设置获取常量值的字段,然后设置获取计算值的字段,然后运行构造函数代码。而且,当然,它必须在某个地方初始化子类。

因此,如果您尝试使用仍在构建的对象,您会在字段中看到奇怪的无效值。这通常不会发生,但方法是:

  • 在分配给另一个字段的过程中引用一个还没有值的字段。

  • 在构造函数中引用一个直到稍后在构造函数中才被分配的值。

  • 在刚刚从 ObjectInputStream 读取的对象中的字段中引用对象中的字段。(OIS 通常需要很长时间才能将值放入它读取的对象中。)

  • 在 Java 5 之前,类似于:

    public volatile MyClass  myObject;
    ...
    myObject = new MyClass( 10 );
    

    可能会造成麻烦,因为另一个线程可以在 MyClass 构造函数完成之前获取对 myObject 的引用,并且它会在对象内部看到错误的值(在这种情况下是零而不是 10)。在 Java 5 中,在构造函数完成之前,JVM 不允许将 myObject 设为非 null。

  • 今天你仍然可以this在构造函数中设置 myObject 并完成同样的事情。

如果你很聪明,你还可以在初始化之前获取 Class 字段。

在您的代码示例中,(n != n)如果某些东西改变了两次读取n. 我猜重点是n从零开始,构造函数将 get 设置为其他值,并assertSanity在构造过程中调用。在这种情况下,n它不是易变的,所以我认为断言永远不会被触发。让它变得不稳定,如果你把所有事情都准确地计时,它就会每百万次左右发生一次。在现实生活中,这种问题经常发生,足以造成严重破坏,但很少发生,以至于您无法重现它。

于 2013-10-10T20:11:35.410 回答
0

我想理论上是可以的。它类似于双重检查锁定问题。

public class Test {
    static Holder holder;

    static void test() {
        if (holder == null) {
            holder = new Holder(1);
        }
        holder.assertSanity();
    }
...

如果 test() 由 2 个线程调用,则线程 2 可能会在初始化仍在进行时看到持有者处于某种状态,因此 n != n 可能恰好为真。这是 n != n 的字节码:

ALOAD 0
GETFIELD x/Holder.n : I
ALOAD 0
GETFIELD x/Holder.n : I
IF_ICMPEQ L1

如您所见,JVM 将字段 n 加载到操作数堆栈两次。因此可能会发生第一个 var 在 init 之前获得值,而在 init 之后获得第二个值

于 2013-10-04T11:38:43.283 回答
0

评论:

Object 构造函数首先在子类构造函数运行之前将默认值写入所有字段

似乎错了。我之前的经验是,类的默认值是在其构造函数运行之前设置的。那是一个超类将在其构造函数运行并执行操作之前看到其初始化变量设置。这是错误的根源,一位朋友查看了基类在构造期间调用超类实现的方法的位置,并将在超类中使用初始化定义的引用设置为 null。该项目将一直存在,直到进入构造函数,此时 init 将其设置为空值。

在完成构造并返回对象引用之前,另一个线程不能使用对该对象的引用(假设在构造函数中没有生成)。

于 2013-10-04T13:41:16.207 回答