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

Ada*_*dam 30 java concurrency multithreading constructor

之前的问题略有不同,但要求是/否答案,但我正在寻找书中遗漏的解释(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);
                }
            });
    }
}
Run Code Online (Sandbox Code Playgroud)

3.2.1.安全施工实践

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

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

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

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

Fed*_*ner 22

请看这篇文章.在那里,它清楚地解释了当你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());
    }
  }
}
Run Code Online (Sandbox Code Playgroud)

编译时,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);
  }
}
Run Code Online (Sandbox Code Playgroud)

接下来我们有匿名的内部类:

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);
  }
}
Run Code Online (Sandbox Code Playgroud)

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

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

'what'基本上是一种竞争条件,NullPointerException在尝试使用该对象时尚未完全初始化时可能导致一个或任何其他异常.在该示例中,如果线程足够快,则可能会doSomething()num尚未正确初始化的情况下运行该方法42.在第一个链接中有一个测试,显示了这一点.

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

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

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


And*_* II 8

我会稍微修改一下这个例子,以便更清楚.考虑这个课程:

public class ThisEscape {

    Object someThing;

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

在幕后,匿名内部类可以访问外部实例.您可以告诉它,因为您可以访问实例变量,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);
    }

}
Run Code Online (Sandbox Code Playgroud)

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

现在考虑两个线程.一个是调用ThisEscape构造函数,另一个是调用processEvent某个事件.另外,假设JVM决定从第一个线程切换到第二个线程,就在source.registerListener线路之后和之前someThing = initTheThing().第二个线程现在运行,它将调用onEvent方法,正如您所看到的,它可以执行某些操作someThing.但是什么someThing呢?它是null,因为另一个线程没有完成初始化对象,所以这可能(可能)导致NullPointerException,这实际上并不是你想要的.

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


Old*_*eon 5

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

想象一下EventSource.registerListener立即调用EventLister.doSomething()!这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.
    }
}
Run Code Online (Sandbox Code Playgroud)

这样做会堵塞漏洞。

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);
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

  • @Magnamag - 请注意,在我的示例中,没有实现 `doSomething`,因为 OP 的示例中没有。这是因为它在哪里并不重要,附加到匿名内部类的 `ThisEscape.this` 指向一个不完整的对象,这就是问题所在。您发布的 Hienz Kabutz 示例清楚地说明了这个问题。我的观点是,使内部类为“static”会删除“this”,从而阻止泄漏。 (3认同)
  • 通常,您可能无法将内部类设为静态,正是因为您需要访问外部类的实例 - 就像在其上调用方法一样。 (2认同)