Java:引用转义
请阅读以下代码是“不安全构造”的示例,因为它允许此引用转义。 我无法理解’这个’是如何逃脱的。 我是java世界的新手。 任何人都可以帮助我理解这一点。
public class ThisEscape { public ThisEscape(EventSource source) { source.registerListener( new EventListener() { public void onEvent(Event e) { doSomething(e); } }); } }
您在问题中发布的示例来自Brian Goetz等人的“Java Concurrency In Practice” 。 它在第3.2节“出版和逃避”中。 我不会尝试在这里重现该部分的细节。 (去买你书架的副本,或者从你的同事那里借一份!)
示例代码说明的问题是构造函数允许在构造函数完成创建对象之前对正在构造的对象的引用进行“转义”。 这是一个问题,原因有两个:
-
如果引用转义,则可以在构造函数完成初始化之前使用该对象,并在不一致(部分初始化)状态下查看它。 即使对象在初始化完成后转义,声明子类也可能导致违反此规则。
-
根据JLS 17.5 ,可以安全地使用对象的最终属性而无需同步。 但是,仅当对象引用在构造函数完成之前未发布(不转义)时才会出现这种情况。 如果你破坏了这个规则,结果就是一个阴险的并发错误,当代码在多核/多处理器机器上执行时可能会让你感到困惑。
ThisEscape
示例是偷偷摸摸的,因为引用是通过隐式传递给匿名EventListener
类构造函数的this
引用转义的。 但是,如果过早明确发布参考文献,也会出现同样的问题。
这是一个例子来说明不完整初始化对象的问题:
public class Thing { public Thing (Leaker leaker) { leaker.leak(this); } } public class NamedThing extends Thing { private String name; public NamedThing (Leaker leaker, String name) { super(leaker); } public String getName() { return name; } }
如果Leaker.leak(...)
方法在泄漏的对象上调用getName()
,它将获得null
…因为在那个时间点对象的构造函数链尚未完成。
这是一个示例,用于说明final
属性的不安全发布问题。
public class Unsafe { public final int foo = 42; public Unsafe(Unsafe[] leak) { leak[0] = this; // Unsafe publication // Make the "window of vulnerability" large for (long l = 0; l < /* very large */ ; l++) { ... } } } public class Main { public static void main(String[] args) { final Unsafe[] leak = new Unsafe[1]; new Thread(new Runnable() { public void run() { Thread.yield(); // (or sleep for a bit) new Unsafe(leak); } }).start(); while (true) { if (leak[0] != null) { if (leak[0].foo == 42) { System.err.println("OK"); } else { System.err.println("OUCH!"); } System.exit(0); } } } }
此应用程序的某些运行可能会打印“OUCH!” 而不是“OK”,表示由于通过leak
arrays的不安全发布,主线程已经在“不可能”状态下观察到Unsafe
对象。 是否发生这种情况取决于您的JVM和硬件平台。
现在这个例子显然是人为的,但不难想象在真正的multithreading应用程序中这种事情会发生什么。
我有同样的怀疑。
事实上,在其他类中实例化的每个类都有一个对变量$this
的封闭类的引用。
这就是java所谓的合成 ,它不是你定义的东西,而是java自动为你做的事情。
如果你想亲眼看到这个,请在doSomething(e)
行中添加一个断点,并检查EventListener
具有的属性。
我的猜测是在ThisEscape
类中声明了doSomething
方法,在这种情况下,引用肯定可以“转义”。
即,某些事件可以在创建之后以及在完成ThisEscape
构造函数的执行之前触发此EventListener
。 反过来,监听器将调用ThisEscape
实例方法。
我会稍微修改你的例子。 现在可以在doSomething
方法中访问变量var
,然后在构造函数中分配它。
public class ThisEscape { private final int var; public ThisEscape(EventSource source) { source.registerListener( new EventListener() { public void onEvent(Event e) { doSomething(e); } } ); // more initialization // ... var = 10; } // result can be 0 or 10 int doSomething(Event e) { return var; } }
在阅读Brian Goetz撰写的“ Java Concurrency In Practice ”时,我遇到了完全相同的问题。
斯蒂芬C的答案(被接受的答案)非常好! 我只想添加一个我发现的资源。 它来自JavaSpecialists ,Heinz M. Kabutz博士正好分析了devnull发布的代码示例。 他解释了编译后生成的类(外部,内部)以及它是如何逃脱的。 我发现解释很有用所以我觉得分享:)
issue192 (他扩展了示例并提供了竞争条件。)
issue192b (他解释了在编译之后生成了什么类的类以及它是如何逃脱的。)