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

时间:2015-02-23 15:06:49

标签: java multithreading concurrency constructor

之前的问题略有不同but asking for a yes/no answer,但我正在寻找书中遗漏的解释(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也是如此   实例。但是对象只能处于可预测的一致状态   在它的构造函数返回之后,从而在其中发布一个对象   构造函数可以发布未完全构造的对象。这是   即使发布是构造函数中的最后一个语句,也是true。   如果此引用在构造期间逃逸,则对象为   被认为没有正确构建。[8]

     

[8]更具体地说,这个参考不应该逃避   线程,直到构造函数返回。这个参考可以是   由构造函数存储在某处,只要它不被使用   另一个线程,直到施工后。清单3.8中的SafeListener   使用这种技术。

     

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

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

3 个答案:

答案 0 :(得分:22)

请参阅this article.明确说明当this逃脱时会发生什么。

这是一个follow-up,有进一步的解释。

这是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

这两篇文章既展示了实际的逃避现象,又展示了可能发生的事情。

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

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

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

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

答案 1 :(得分:8)

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

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决定在source.registerListener行之后和someThing = initTheThing()之前切换从第一个线程切换到第二个线程。现在运行第二个线程,它将调用onEvent方法,正如您所看到的,它会对someThing执行某些操作。但是什么是someThing?它是null,因为另一个线程没有完成初始化对象,所以这可能(可能)导致NullPointerException,这实际上并不是你想要的。

总结一下:注意不要转义尚未完全初始化的对象(换句话说,它们的构造函数尚未完成)。你可能无意中做到这一点的一个微妙方法是从构造函数中转义匿名内部类,它将隐式地转义未完全初始化的外部实例。

答案 2 :(得分:4)

这里的关键点是,通常很容易忘记内联匿名对象仍然具有对其父对象的引用,以及该代码片段如何暴露尚未完全初始化的内容本身的实例。

想象一下,EventSource.registerListener立即拨打EventLister.doSomething()! <{1}}将在其父doSomething不完整的对象上调用。

this

这样做会堵塞漏洞。

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.
    }
}