Java 如何通过发布内部类实例来转义对外部类的`this`引用?
这与前面的问题略有不同,但我正在寻找本书(实践中的Java并发性)中缺少的解释,即如何恶意或意外地利用这个明显的大错误 一种最终的机制,通过它可以改变一个对象或其内部状态 published是发布内部类实例,如中所示 清单3.7中的这个Escape。当ThisEscape发布 EventListener,它隐式发布封闭的ThisEscape 实例,因为内部类实例包含隐藏的 对封闭实例的引用 清单3.7。隐式地允许此引用转义。不要 这样做 3.2.1。安全施工规范 此escape说明了当 此引用在构造过程中逃逸。当内心 EventListener实例已发布,封闭的ThisEscape也已发布 例如。但对象仅处于可预测、一致的状态 在其构造函数返回后,从其 构造函数可以发布未完全构造的对象。这是 即使发布是构造函数中的最后一条语句,也为true。 如果此引用在构造期间转义,则对象为 被认为结构不合理[8] [8] 更具体地说,此引用不应从 线程,直到构造函数返回。此参考文件的内容可以是 由构造函数存储在某个地方,只要它不被 另一个线程,直到施工后。清单3.8中的SafeListener 使用这种技术 在施工过程中,不要让此参考泄漏Java 如何通过发布内部类实例来转义对外部类的`this`引用?,java,multithreading,concurrency,constructor,Java,Multithreading,Concurrency,Constructor,这与前面的问题略有不同,但我正在寻找本书(实践中的Java并发性)中缺少的解释,即如何恶意或意外地利用这个明显的大错误 一种最终的机制,通过它可以改变一个对象或其内部状态 published是发布内部类实例,如中所示 清单3.7中的这个Escape。当ThisEscape发布 EventListener,它隐式发布封闭的ThisEscape 实例,因为内部类实例包含隐藏的 对封闭实例的引用 清单3.7。隐式地允许此引用转义。不要 这样做 3.2.1。安全施工规范 此escape说明了当 此引用在
在构建完成之前,有人会如何编写代码来阻止它到达外部类?第一段中以斜体字提到的
隐藏的内部类引用是什么?
我将稍微修改一下示例,使其更清楚。考虑这个类:
public class ThisEscape {
Object someThing;
public ThisEscape(EventSource source) {
source.registerListener(
new EventListener() {
public void onEvent(Event e) {
doSomething(e, someThing);
}
});
someThing = initTheThing();
}
}
在幕后,匿名内部类可以访问外部实例。您可以这样说,因为您可以访问实例变量someThing
,并且正如Shashank提到的,您可以通过thissecape.this
访问外部实例
问题是,通过将匿名内部类实例提供给外部(在本例中为EventSource
对象),它还将携带thisecape实例
有什么不好的地方吗?考虑以下事件源的实现:
public class SomeEventSource implements EventSource {
EventListener listener;
public void registerListener(EventListener listener) {
this.listener = listener;
}
public void processEvent(Event e) {
listener.onEvent(e);
}
}
在thisecape
的构造函数中,我们注册了一个EventListener
,它将存储在listener
实例变量中
现在考虑两个线程。一个调用
thisecape
构造函数,而另一个调用processEvent
并带有一些事件。另外,假设JVM决定从第一个线程切换到第二个线程,就在source.registerListener
行之后,就在someThing=initTheThing()之前。第二个线程现在运行,它将调用onEvent方法,正如您所看到的,它使用something
执行某些操作。但是什么是东西
?它是null,因为另一个线程没有完成初始化对象,所以这(可能)会导致NullPointerException,这实际上不是您想要的
总而言之:注意不要转义尚未完全初始化的对象(或者换句话说,它们的构造函数尚未完成)。一种不经意间可以做到这一点的微妙方法是从构造函数中转义匿名内部类,这将隐式转义外部实例,它没有完全初始化。这里的关键点是,通常很容易忘记内嵌匿名对象仍然有一个对其父对象的引用,这就是此代码片段如何公开尚未完全初始化的自身实例
想象一下EventSource.registerListener
立即调用EventLister.doSomething()
!将对其父对象此
不完整的对象调用该doSomething
public class ThisEscape {
public ThisEscape(EventSource source) {
// Calling a method
source.registerListener(
// With a new object
new EventListener() {
// That even does something
public void onEvent(Event e) {
doSomething(e);
}
});
// While construction is still in progress.
}
}
这样做会堵塞漏洞
public class TheresNoEscape {
public TheresNoEscape(EventSource source) {
// Calling a method
source.registerListener(
// With a new object - that is static there is no escape.
new MyEventListener());
}
private static class MyEventListener {
// That even does something
public void onEvent(Event e) {
doSomething(e);
}
}
}
请看这里,它清楚地解释了当你让这个
逃逸时会发生什么
下面是一个有进一步解释的例子
这是海因茨卡布兹惊人的时事通讯,这里讨论了这个和其他非常有趣的话题。我极力推荐
以下是从链接中获取的示例,它显示了this
引用如何转义:
public class ThisEscape {
private final int num;
public ThisEscape(EventSource source) {
source.registerListener(
new EventListener() {
public void onEvent(Event e) {
doSomething(e);
}
});
num = 42;
}
private void doSomething(Event e) {
if (num != 42) {
System.out.println("Race condition detected at " +
new Date());
}
}
}
编译后,javac生成两个类。外部类如下所示:
接下来是匿名内部类:
在这里,在外部类的构造函数中创建的匿名内部类被转换为一个包访问类,该类接收对外部类的引用(允许this
转义的类)。为了使内部类能够访问外部类的属性和方法,将在外部类中创建一个静态包访问方法。这是访问$000
这两篇文章展示了实际逃逸是如何发生的以及可能发生的情况
“what”基本上是一个争用条件,在尚未完全初始化的情况下尝试使用对象时,可能会导致NullPointerException
或任何其他异常。在本例中,如果线程速度足够快,则可能会在num
尚未正确初始化为42
时运行doSomething()
方法。在第一个链接中,有一个测试正好显示了这一点
编辑:
缺少关于如何针对此问题/功能编写代码的几行代码。我只能考虑坚持一套(可能不完整的)规则
public class ThisEscape {
private final int num;
public ThisEscape(EventSource source) {
source.registerListener(
new EventListener() {
public void onEvent(Event e) {
doSomething(e);
}
});
num = 42;
}
private void doSomething(Event e) {
if (num != 42) {
System.out.println("Race condition detected at " +
new Date());
}
}
}
public class ThisEscape {
private final int num;
public ThisEscape(EventSource source) {
source.registerListener(new ThisEscape$1(this));
num = 42;
}
private void doSomething(Event e) {
if (num != 42)
System.out.println(
"Race condition detected at " + new Date());
}
static void access$000(ThisEscape _this, Event event) {
_this.doSomething(event);
}
}
class ThisEscape$1 implements EventListener {
final ThisEscape this$0;
ThisEscape$1(ThisEscape thisescape) {
this$0 = thisescape;
super();
}
public void onEvent(Event e) {
ThisEscape.access$000(this$0, e);
}
}