对象创建(状态初始化)和线程安全

我正在研究“实践中的Java并发”这本书,发现在下面引用的声明中很难相信(但不幸的是它有意义)。

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构造函数首先将默认值写入所有字段。 因此,可以将字段的默认值视为过时值

关于上面的粗体陈述,

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

你误读了这本书。 它明确地说:

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

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

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

因此,如果您尝试使用仍在构造的对象,则可以在字段中看到奇数,无效的值。 这通常不会发生,但是要做到这一点:

  • 引用在分配给另一个字段期间尚未具有值的字段。

  • 引用构造函数中的值,该值在构造函数中稍后才会被赋值。

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

  • 在Java 5之前,类似于:

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

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

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

如果你很聪明,你也可以在初始化之前掌握Class字段。

在您的代码示例中,如果某些内容更改了n的两次读取之间的值,则(n != n)将为true。 我想点是n开始为零,构造函数将其设置为其他东西,并在构造期间调用assertSanity 。 在这种情况下, n不是易失性的,所以我认为断言不会被触发。 让它变得不稳定,如果你准确地计算一切,它会每百万次左右发生一次。 在现实生活中,这种问题经常发生,足以造成严重破坏,但很少发生,你无法重现它。

我猜理论上有可能。 它类似于双重检查锁定问题。

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

如果2个线程调用test(),则线程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之后获得第二个值

评论:

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

似乎错了。 我之前的经验是,在运行构造函数之前设置类的默认值。 这是一个超级类,它会在构造函数运行之前看到它的初始化变量。 这是朋友查看基类在构造过程中调用方法的bug的根,超类实现并在超类中将初始化定义的引用设置为null。 该项将存在,直到进入构造函数,此时init将其设置为null值。

对象的引用不可用于另一个线程(假设在构造函数中没有生成),直到它完成构造并返回对象引用。