我正在研究“实践中的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(在锁定保护的单个同步块中调用超级构造函数),但是什么是解决方案?想象一个具有多个级别的类层次结构(即使不推荐,也可以假设它是可能的)。上面的代码片段是我们在大多数项目中每天都看到的一种原型。
答案 0 :(得分:2)
这里的问题不是Holder类本身,而是Holder没有正确发布。
所以上面的构造如果好的话。什么不好是将这样的对象不正确地发布到其他线程。这本书详细解释了这一点。
答案 1 :(得分:1)
创建新对象时,事情会顺序发生。我不知道精确的顺序,但它是这样的:分配空间并将其初始化为零,然后设置获取常量值的字段,然后设置获得计算值的字段,然后运行构造函数代码。当然,它必须在某处初始化子类。
因此,如果您尝试使用仍在构造的对象,则可以在字段中看到奇数,无效的值。这通常不会发生,但是要做到这一点:
引用在分配给其他字段时尚未拥有值的字段。
引用构造函数中的值,该值在构造函数中稍后才会被赋值。
引用刚刚从ObjectInputStream读取的对象中的字段中的对象中的字段。 (OIS通常需要很长时间才能将值放在它所读取的对象中。)
在Java 5之前,类似于:
public volatile MyClass myObject;
...
myObject = new MyClass( 10 );
可能会造成麻烦,因为另一个线程可以在MyClass构造函数完成之前获取对myObject的引用,并且它会在对象内部看到错误的值(在这种情况下为零而不是10)。对于Java 5,在构造函数完成之前,不允许JVM使myObject为非空。
今天你仍然可以在构造函数中将myObject设置为this
并完成同样的事情。
如果你很聪明,你也可以在初始化之前掌握Class字段。
在您的代码示例中,如果某些内容更改了(n != n)
的两次读取之间的值,则n
将为true。我想点{{1}}开始为零,构造函数将其设置为其他内容,并在构造期间调用n
。在这种情况下,assertSanity
不是易失性的,所以我认为断言不会被触发。让它变得不稳定,如果你准确地计算一切,它会每百万次左右发生一次。在现实生活中,这种问题经常发生,只会造成严重破坏,但很少,你无法重现它。
答案 2 :(得分:0)
我猜理论上有可能。它类似于双重检查锁定问题。
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之后获得第二个
答案 3 :(得分:0)
评论:
Object构造函数首先将默认值写入所有字段 在子类构造函数运行之前
似乎错了。我之前的经验是,在运行构造函数之前设置类的默认值。这是一个超级类,它会在构造函数运行之前看到它的初始化变量。这是朋友查看基类在构造过程中调用方法的bug的根,超类实现并在超类中将初始化定义的引用设置为null。该项将存在,直到进入构造函数,此时init将其设置为null值。
对象的引用不可用于另一个线程(假设在构造函数中没有生成),直到它完成构造并返回对象引用。