“this”引用外部类如何通过发布内部类实例进行转义?

这在前面被问得略有不同,但要求一个是/否的答案,但我正在寻找书中缺少的解释(Java并发实践),这个明显的大错误将如何被恶意或意外地利用。

发布对象或其内部状态的最后一种机制是发布内部类实例,如清单 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);
                }
            });
    }
}

安全施工实践

ThisEscape 说明了逃逸的一个重要特例 — 当 this 引用在构造过程中逃逸时。当内部 EventListener 实例发布时,封闭的 ThisEscape 实例也是如此。但是,只有在构造函数返回后,对象才处于可预测的一致状态,因此从其构造函数中发布对象可以发布不完整构造的对象。即使发布是构造函数中的最后一个语句,也是如此。如果此引用在构造过程中转义,则认为对象未正确构造。[8]

[8] 更具体地说,在构造函数返回之前,this 引用不应该从线程中转义。构造函数可以将 this 引用存储在某个位置,只要在构造之前不被另一个线程使用即可。清单 3.8 中的 SafeListener 使用此技术。

在施工过程中,不要让此引用逃逸。

有人如何对此进行编码以在完成构造之前访问OuterClass?第一段中斜体字中提到的隐藏的内部类引用是什么?


答案 1

请参阅此文章。在那里,它清楚地解释了当你逃脱时会发生什么。this

以下是后续进一步解释。

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

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

这两篇文章展示了实际的逃逸是如何发生的,以及可能发生的情况。

“what”基本上是一个争用条件,当尝试在尚未完全初始化的情况下使用对象时,可能会导致或任何其他异常。在此示例中,如果线程足够快,则可能会发生它运行方法的情况,而尚未正确初始化为 。在第一个链接中,有一个测试准确地显示了这一点。NullPointerExceptiondoSomething()num42

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

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

如果需要执行其他操作,请使用生成器或工厂模式。


答案 2

我将稍微修改一下示例,以使其更清晰。请考虑以下类:

public class ThisEscape {

    Object someThing;

    public ThisEscape(EventSource source) {
        source.registerListener(
            new EventListener() {
                public void onEvent(Event e) {
                    doSomething(e, someThing);
                }
            });
        someThing = initTheThing();
    }
}

在后台,匿名内部类可以访问外部实例。您可以知道这一点,因为您可以访问实例变量,并且正如Shashank所提到的,您可以通过 访问外部实例。someThingThisEscape.this

问题在于,通过将匿名内部类实例提供给外部(在本例中为对象),它还将携带 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);
    }

}

在 的构造函数中,我们注册一个将存储在实例变量中的构造函数。ThisEscapeEventListenerlistener

现在考虑两个线程。一个是调用构造函数,而另一个调用某个事件。另外,假设 JVM 决定从第一个线程切换到第二个线程,紧挨着该行,紧挨着行,就在 之前。第二个线程现在运行,它将调用 onEvent 方法,如您所见,该方法对 执行某些操作。但什么是?它是空的,因为另一个线程没有完成对象的初始化,所以这将(可能)导致NullPointerException,这实际上不是你想要的。ThisEscapeprocessEventsource.registerListenersomeThing = initTheThing()someThingsomeThing

总而言之:请注意不要转义尚未完全初始化的对象(或者换句话说,它们的构造函数尚未完成)。一种无意中可能这样做的微妙方法是从构造函数中转义匿名内部类,这将隐式转义未完全初始化的外部实例。


推荐