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

Java 如何通过发布内部类实例来转义对外部类的`this`引用?,java,multithreading,concurrency,constructor,Java,Multithreading,Concurrency,Constructor,这与前面的问题略有不同,但我正在寻找本书(实践中的Java并发性)中缺少的解释,即如何恶意或意外地利用这个明显的大错误 一种最终的机制,通过它可以改变一个对象或其内部状态 published是发布内部类实例,如中所示 清单3.7中的这个Escape。当ThisEscape发布 EventListener,它隐式发布封闭的ThisEscape 实例,因为内部类实例包含隐藏的 对封闭实例的引用 清单3.7。隐式地允许此引用转义。不要 这样做 3.2.1。安全施工规范 此escape说明了当 此引用在

这与前面的问题略有不同,但我正在寻找本书(实践中的Java并发性)中缺少的解释,即如何恶意或意外地利用这个明显的大错误

一种最终的机制,通过它可以改变一个对象或其内部状态 published是发布内部类实例,如中所示 清单3.7中的这个Escape。当ThisEscape发布 EventListener,它隐式发布封闭的ThisEscape 实例,因为内部类实例包含隐藏的 对封闭实例的引用

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

3.2.1。安全施工规范

此escape说明了当 此引用在构造过程中逃逸。当内心 EventListener实例已发布,封闭的ThisEscape也已发布 例如。但对象仅处于可预测、一致的状态 在其构造函数返回后,从其 构造函数可以发布未完全构造的对象。这是 即使发布是构造函数中的最后一条语句,也为true。 如果此引用在构造期间转义,则对象为 被认为结构不合理[8]

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

在施工过程中,不要让此参考泄漏


在构建完成之前,有人会如何编写代码来阻止它到达外部类?第一段中以斜体字提到的
隐藏的内部类引用是什么?

我将稍微修改一下示例,使其更清楚。考虑这个类:

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