`this`如何通过发布内部类实例引用外部类转义?

之前的问题略有不同, 但要求是/否答案,但我正在寻找书中遗漏的解释(Java Concurrency in Practice),这个明显的大错误将如何被恶意或意外地利用。

可以发布对象或其内部状态的最终机制是发布内部类实例,如清单3.7中的ThisEscape所示。 当ThisEscape发布EventListener时,它也隐式发布封闭的ThisEscape实例,因为内部类实例包含对封闭实例的隐藏引用

清单3.7。 隐式允许此引用转义。 不要这样做。

public class ThisEscape { public ThisEscape(EventSource source) { source.registerListener( new EventListener() { public void onEvent(Event e) { doSomething(e); } }); } } 

3.2.1。 安全施工实践

ThisEscape说明了一个重要的特殊情况 – 当它在构造过程中引用转义时。 发布内部EventListener实例时,封闭的ThisEscape实例也是如此。 但是,只有在构造函数返回后,对象才处于可预测的一致状态,因此从构造函数中发布对象可以发布未完全构造的对象。 即使发布是构造函数中的最后一个语句,也是如此。 如果此参考在施工期间逃逸,则认为该物体构造不当。[8]

[8]更具体地说,在构造函数返回之前,此引用不应从线程中转义。 这个引用可以由构造函数存储在某处,只要它在构造之后不被另一个线程使用。 清单3.8中的SafeListener使用了这种技术。

在施工期间不要让此参考物逃逸。

在完成构建之前,有人会如何编写代码来到OuterClass? 在第一段中用斜体字提到的hidden inner class reference是什么?

请看这篇文章。 在那里,它清楚地解释了当你让它逃脱时会发生什么。

以下是进一步解释的后续行动 。

这是Heinz Kabutz惊人的时事通讯,讨论了这个和其他非常有趣的话题。 我强烈推荐它。

以下是从链接中获取的示例,其中显示了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会生成两个类。 外部类看起来像这样:

 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); } } 

这里,在外部类的构造函数中创建的匿名内部类被转换为一个包访问类,该类接收对外部类的引用(允许this转义的类)。 要使内部类能够访问外部类的属性和方法,可以在外部类中创建静态包访问方法。 这是access$000

这两篇文章既展示了实际的逃逸行为,又展示了可能发生的事情。

‘what’基本上是一种竞争条件,在未完全初始化时尝试使用该对象时可能导致NullPointerException或任何其他exception。 在该示例中,如果线程足够快,则可能发生它运行doSomething()方法而num尚未正确初始化为42 。 在第一个链接中,有一个测试可以准确显示。

编辑:缺少关于如何针对此问题/function进行编码的几行。 我只能考虑坚持一套(可能是不完整的)规则/原则来避免这个问题和其他人一样:

  • 仅从构造函数中调用private方法
  • 如果您喜欢肾上腺素并且想要在构造函数中调用protected方法,请执行此操作,但将这些方法声明为final ,以便它们不能被子类覆盖
  • 永远不要在构造函数中创建内部类,无论是匿名,本地,静态还是非静态
  • 在构造函数中,不要将this作为参数直接传递给任何东西
  • 避免上述规则的任何可传递组合,即不要在构造函数中调用的privateprotected final方法中创建匿名内部类
  • 使用构造函数只构造一个类的实例,并让它只使用默认值或提供的参数初始化类的属性

如果您需要做更多事情,请使用构建器或工厂模式。

我会稍微修改一下这个例子,以使其更清晰。 考虑这个课程:

 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所说,你可以通过ThisEscape.this访问外部实例。

问题是通过将匿名内部类实例提供给外部(在本例中为EventSource对象),它还将携带ThisEscape实例。

它会发生什么不好的事情? 考虑下面的EventSource的这个实现:

 public class SomeEventSource implements EventSource { EventListener listener; public void registerListener(EventListener listener) { this.listener = listener; } public void processEvent(Event e) { listener.onEvent(e); } } 

ThisEscape的构造函数中,我们注册了一个EventListener ,它将存储在listener实例变量中。

现在考虑两个线程。 一个是调用ThisEscape构造函数,而另一个调用processEvent有一些事件。 另外,假设JVM决定从第一个线程切换到第二个线程, someThing = initTheThing()someThing = initTheThing()行之后,在someThing = initTheThing() 。 现在运行第二个线程,它将调用onEvent方法,正如您所看到的,它可以使用someThing执行某些someThing 。 但是什么是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); } } }