java 对象在构造过程中何时变为非空?
Posted
技术标签:
【中文标题】java 对象在构造过程中何时变为非空?【英文标题】:When does a java object become non-null during construction? 【发布时间】:2010-10-15 11:26:41 【问题描述】:假设你正在像这样创建一个 java 对象:
SomeClass someObject = null;
someObject = new SomeClass();
someObject 在什么时候变为非空?是在SomeClass()
构造函数运行之前还是之后?
稍微澄清一下,假设另一个线程要检查 someObject
是否为 null 而 SomeClass()
构造函数完成一半,它是 null 还是非 null?
另外,如果someObject
是这样创建的,会有什么不同:
SomeClass someObject = new SomeClass();
someObject
会不会为空?
【问题讨论】:
(如果是局部变量,在构造函数完成之前是无法观察到的。) 它可以是实例变量或静态成员。 【参考方案1】:如果另一个线程“在”构造过程中检查someObject
变量,我相信它可能(由于内存模型中的怪癖)看到一个部分初始化的对象。新的(从 Java 5 开始)内存模型意味着任何 final 字段都应该在对象对其他线程可见之前设置为它们的值(只要对新创建对象的引用不以任何其他方式从构造函数中逃脱)但除此之外没有太多保证。
基本上,不要在没有适当锁定(或静态初始化器等提供的保证)的情况下共享数据 :) 严重的是,内存模型非常棘手,一般来说无锁编程也是如此。尽量避免这种可能性。
在逻辑术语中,赋值发生在构造函数运行之后 - 所以如果你观察变量来自同一个线程,它将在期间为空构造函数调用。但是,正如我所说,内存模型有些奇怪。
编辑:出于双重检查锁定的目的,您可以摆脱这个 if 您的字段是 volatile
并且 if 您正在使用 Java 5 或更高。在 Java 5 之前,内存模型还不够强大。但是,您需要完全正确地获得模式。有关详细信息,请参阅 Effective Java,第 2 版,第 71 项。
编辑:这是我反对 Aaron 的内联在单个线程中可见的理由。假设我们有:
public class FooHolder
public static Foo f = null;
public static void main(String[] args)
f = new Foo();
System.out.println(f.fWasNull);
// Make this nested if you like, I don't believe it affects the reasoning
public class Foo
public boolean fWasNull;
public Foo()
fWasNull = FooHolder.f == null;
我相信这会总是报告true
。来自section 15.26.1:
否则,需要三个步骤:
首先,计算左侧操作数以生成变量。如果此评估完成 突然,然后分配 表达式突然完成 同样的原因;右手操作数是 未评估且未分配 发生。 否则,将计算右侧操作数。如果这 评估突然完成,然后 赋值表达式完成 突然出于同样的原因,没有 分配发生。 否则,右手边的值 操作数转换为的类型 左边的变量,受到 到值集转换(§5.1.13)到 适当的标准值集 (不是扩展指数值集), 转换的结果是 存储到变量中。
然后来自section 17.4.5:
两个动作可以通过happens-before关系排序。如果一个动作发生在另一个动作之前,那么第一个动作对第二个动作可见并在第二个动作之前排序。
如果我们有两个动作 x 和 y,我们写 hb(x, y) 来表示 x 发生在 y 之前。
如果 x 和 y 是同一线程的操作,并且 x 在程序顺序中位于 y 之前,则为 hb(x, y)。 从对象的构造函数的末尾到该对象的终结器(第 12.6 节)的开头之间存在发生前边缘。 如果动作 x 与后续动作 y 同步,那么我们也有 hb(x, y)。 如果 hb(x, y) 和 hb(y, z),则 hb(x, z)。需要注意的是,两者之间存在发生前的关系 行动并不一定意味着它们必须以这种顺序发生 执行。如果重新排序产生与合法执行一致的结果, 这并不违法。
换句话说,即使在单个线程中也可以发生奇怪的事情,但不能观察到。在这种情况下,差异会是可以观察到的,这就是为什么我认为它是非法的。
【讨论】:
什么样的怪癖(我不太懂Java)?我很想知道什么会允许这样的事情,因为您会认为由于构造函数尚未完成运行,因此对变量的赋值还没有发生,这意味着变量仍然为空。 是的,那里没有“之前发生”。 final 字段有一些额外的保证。一般来说,不要编写带有种族的代码。 Tom:你能仔细检查我在这里写的内容吗?我不愿意非常明确地谈论这种事情...... @Andrew:构造函数实际上可能已经完成,并且赋值本身是可见的,但是在构造期间编写的其他字段可能对其他人不可见线程尚未。 看我的 cmets 来回答你的问题。我不相信你对为什么双重检查锁定被破坏的理解是完全正确的。【参考方案2】:someObject
将在构建过程中的某个时刻变为非null
。通常有两种情况:
-
优化器已内联构造函数
构造函数未内联。
在第一种情况下,VM 会执行这段代码(伪代码):
someObject = malloc(SomeClass.size);
someObject.field = ...
....
所以在这种情况下,someObject
不是null
并且它指向的内存不是 100% 初始化的,即不是所有的构造函数代码都已运行!这就是double-checked locking 不起作用的原因。
在第二种情况下,来自构造函数的代码将运行,引用将被传回(就像在正常的方法调用中一样)并且 someObject 将被设置为引用的值 after所有初始化代码都已运行。
问题是没有办法告诉java不要提前分配someObject
。例如,您可以尝试:
SomeClass tmp = new SomeClass();
someObject = tmp;
但是由于没有使用 tmp,所以允许优化器忽略它,所以它会产生与上面相同的代码。
所以这种行为是为了让优化器生成更快的代码,但在编写多线程代码时它可能会给你带来麻烦。在单线程代码中,这通常不是问题,因为在构造函数完成之前不会执行任何代码。
[编辑] 这是一篇很好的文章,它解释了正在发生的事情:http://www.ibm.com/developerworks/java/library/j-dcl.html
PS:Joshua Bloch 的“Effective Java, Second Edition”一书包含 Java 5 及更高版本的解决方案:
private volatile SomeClass field;
public SomeClass getField ()
SomeClass result = field;
if (result == null) // First check, no locking
synchronized(this)
result = field;
if (result == null) // second check with locking
field = result = new SomeClass ();
return result;
看起来很奇怪,但应该适用于每个 Java VM。请注意,每一点都很重要;如果您省略双重分配,您将获得糟糕的性能或部分初始化的对象。如需完整说明,请购买该书。
【讨论】:
我不相信这是真正的内联问题,因为这样效果将在同一个线程中可见(我不相信它可以)。取而代之的是内存模型的变幻莫测,即首先将写入发布到其他线程。 特别是,我不相信执行构造的线程可以在构造函数期间将 someObject 视为非空,这与您的伪代码相反。如果您认为它可以看到非空值,请给出示例代码:) 正如我所说,在单线程代码中,内联通常不会导致任何问题。但是当你有多个线程时,没有启动构造函数的线程可以看到一个部分构造的对象。 我认为您只是在转移问题。在分配 mallocs 值之前,VM 中 someObject 的值是多少?此外,我怀疑优化器意识到 null 值将永远不会被使用,并将第一个示例优化为第二种形式,这会使 someObject 在一段时间内为 null。 我的论点是你不应该依赖它,即使它在单线程代码中工作。一旦你需要多线程,你就会看到非常奇怪的错误。因此,永远不要依赖编写取决于引用变为非空的时间的代码。【参考方案3】:someObject
将是一个空指针,直到它从该类型的构造函数分配一个指针值。由于分配是从右到左的,因此可能让另一个线程在构造函数仍在运行时检查someObject
。这将在分配指向变量的指针之前,因此someObject
仍然为空。
【讨论】:
【参考方案4】:在另一个线程中,在构造函数完成执行之前,您的对象仍将显示为 null。这就是为什么如果构造被异常终止,引用将保持为空。
Object o = null;
try
o = new CtorTest();
catch (Exception e)
assert(o == null); // i will be null
在哪里
class CtorTest
public CtorTest()
throw new RuntimeException("Ctor exception.");
确保在另一个对象上同步,而不是正在构造的对象。
【讨论】:
这很有趣; o 在构造函数运行时可以是 != null 但我猜如果你有一个 try-catch,优化器会将引用分配给一个临时变量。【参考方案5】:对于您的第一个示例: someObject 在构造函数完成后变为非空。如果您要从另一个线程进行检查,则 someObject 在构造函数完成后将变为非空。请注意,您永远不应该从不同的线程访问未同步的对象,因此您的示例不应该在实际代码中以这种方式实现。
对于第二个示例, someObject 永远不会为 null,因为它是在 SomeClass 本身被构造并且 someObject 被创建并使用新创建的对象初始化之后构造的。线程也一样:不要在没有同步的情况下从不同的线程访问这个变量!
【讨论】:
【参考方案6】:下面是一些测试代码,它显示在构造函数完成运行之前对象为空:
public class Test
private static SlowlyConstructed slowlyConstructed = null;
public static void main(String[] args)
Thread constructor = new Thread()
public void run()
Test.slowlyConstructed = new SlowlyConstructed();
;
Thread checker = new Thread()
public void run()
for(int i = 0; i < 10; i++)
System.out.println(Test.slowlyConstructed);
try Thread.sleep(1000);
catch(Exception e)
;
checker.start();
constructor.start();
private static class SlowlyConstructed
public String s1 = "s1 is unset";
public String s2 = "s2 is unset";
public SlowlyConstructed()
System.out.println("Slow constructor has started");
s1 = "s1 is set";
try Thread.sleep(5000);
catch (Exception e)
s2 = "s2 is set";
System.out.println("Slow constructor has finished");
public String toString()
return s1 + ", " + s2;
输出:
空值 缓慢的构造函数已启动 空值 空值 空值 空值 空值 慢构造器已完成 s1 已设置,s2 已设置 s1 已设置,s2 已设置 s1 已设置,s2 已设置 s1 已设置,s2 已设置【讨论】:
你不能以这种方式进行证明。 这有什么问题?他问它什么时候发生,我写了一些代码来显示它什么时候发生。 由于线程的性质和不同 VM 的不同优化,您无法通过简单的代码示例证明或反驳该问题。以上是关于java 对象在构造过程中何时变为非空?的主要内容,如果未能解决你的问题,请参考以下文章