【问题标题】:How does `this` reference to an outer class escape through publishing inner class instance?`this` 引用外部类如何通过发布内部类实例转义?
【发布时间】:2015-04-24 22:54:27
【问题描述】:

早前but asking for a yes/no answer 提出的问题略有不同,但我正在寻找书中(Java 并发实践)中缺少的解释,说明这个明显的大错误将如何被恶意或意外利用。

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

清单 3.7。隐式允许 this 引用转义。别 这样做。

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

3.2.1。安全施工实践

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

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

在构造过程中不允许 this 引用逃逸。

如何在 OuterClass 完成构建之前对其进行编码以到达 OuterClass?第一段斜体中提到的hidden inner class reference是什么?

【问题讨论】:

  • 访问外部类实例的语法是OuterClassName.this。我不明白你的其余问题。
  • 您的代码示例无法编译。

标签: java multithreading concurrency constructor


【解决方案1】:

请看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 方法中创建匿名内部类
  • 使用构造函数只构造一个类的实例,并让它只初始化类的属性,无论是使用默认值还是使用提供的参数

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

【讨论】:

  • 你能解释一下如果我们用方法引用或 lamdas 替换匿名类,那么这段代码的行为方式和原因是什么?
  • @MandeepRajpal 那将是一个不同的问题。请发布它,并参考这个问题
【解决方案2】:

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

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,这实际上不是您想要的。

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

【讨论】:

    【解决方案3】:

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

    想象一下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.
        }
    }
    

    这样做会堵住漏洞。

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

    【讨论】:

    • 你的意思可能是ThisEscape.doSomething()
    • @assylias - 这会让犯罪更清楚,但 OP 正在询问 封闭 this 转义。
    • 您确定MyEventListener 类没有对其封闭的TheresNoEscape 类的隐藏引用吗?如果TheresNoEscape外部类的this引用没有转义,那么MyEventListener内部类怎么调用属于外部类的doSomething(e)方法呢?
    • @Magnamag - 请注意在我的示例中没有实现 doSomething,因为 OP 的示例中没有。这是因为它在哪里并不重要,附加到匿名内部类的ThisEscape.this 指向一个不完整的对象,这就是问题所在。您发布的 Hienz Kabutz 示例清楚地表明了这个问题。我的观点是,使内部类 static 删除 this 并因此停止泄漏。
    • 通常您可能无法将内部类设为静态,这正是因为您需要访问外部类的实例——例如调用其上的方法。
    【解决方案4】:

    据我了解,问题在于用户可以访问转义的ThisEscape 引用(毕竟,据说它是隐藏的)。问题在于,由于类的编译和执行方式,JVM 可以看到该引用并在构造函数完成构建 ThisEscape 实例之前开始在其他地方引用它。因此可能会产生竞争条件。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2011-02-13
      • 1970-01-01
      • 2010-09-23
      • 1970-01-01
      相关资源
      最近更新 更多