通过匿名类逃逸封闭对象的引用 - Java

6

我正在阅读《Java并发实践》,以下示例来自该书。我的问题是:什么是引用逃逸?这会造成什么问题?this引用如何从doSomething(e)中逃逸出来。

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

这怎么解决问题呢?
public class SafeListener {
    private final EventListener listener;
    private SafeListener() {
        listener = new EventListener() {
            public void onEvent(Event e) {
                doSomething(e);
            }
        };
    }
    public static SafeListener newInstance(EventSource source) {
        SafeListener safe = new SafeListener();
        source.registerListener(safe.listener);
        return safe;
    }
}

编辑:

我尝试了下面的例子

public class Escape {
    public  Escape( Printer printer ){
        printer.print(new Escaper(){
            @Override
            public void parentData(){
            theCulprit1(Escape.this);
            }
            public String name = "shal";
            @Override
            public void theCulprit(){
            System.out.println( this.name );
            System.out.println( Escape.this.age );
            }
        });
        canAccess();
    }
    public void  canAccess(){
    this.age = "25";
    }
    public String age = "62";
    @SuppressWarnings("unused")
    public static void main(String args[]){
    Escape escape = new Escape(new Printer());
    }
}

class Printer{
    public void print(Escaper escaper){
    escaper.theCulprit();
    escaper.parentData();
    }
}

class Escaper{
    public void parentData(){
    }
    public void theCulprit(){
    }
    public void theCulprit1(Escape escape){
    System.out.println(escape.age);
    }
}

由于逃逸对象的不完整构建,这导致输出 shal 62 62。而当我将代码更改如下时:
public class Escape {
    private final Escaper escaper;
    private Escape( ){
        escaper = new Escaper(){
            @Override
            public void parentData(){
            theCulprit1(Escape.this);
            }
            public String name = "shal";
            public void theCulprit(){
            System.out.println( name );
            System.out.println( age );
            }
        };
        canAccess();
    }
    public void  canAccess(){
    age = "25";
    }
    public String age = "62";
    public static Escape newInstance( Printer printer){
    Escape escape = new Escape();
    printer.print(escape.escaper);
    return escape;
    }
    @SuppressWarnings("unused")
    public static void main(String args[]){
    Escape.newInstance(new Printer());
    }
}

这里,它输出 shal 25 25。

我是正确的吗? 还有操作的重新排序吗?因为在第一个例子中,年龄被初始化为62岁。 即使在我的第二个例子中没有将转义器字段设置为final,它也可以正常工作!


语言?平台?你实际上在这里问什么? - Oded
2个回答

8
在第一种形式中,事件监听器对象在构造函数中注册到事件源之前,并在此使自身(以及与之关联的“this”对象)可用于事件源。如果内部类对象逃逸,则外部对象也会逃逸。
为什么这是个问题?一旦事件监听器被注册,事件源可能随时调用它的方法。想象一下,事件源正在使用的线程开始调用事件监听器方法,这甚至可能发生在构造函数完成之前。
然而,由于可见性问题,这个问题比看起来更加严重。即使您让注册成为构造函数最后执行的操作,仍有可能看到局部构造的对象或处于无效状态的对象。在缺乏适当的happens-before序列的情况下,根本没有可见性保证。
将其声明为final可以提供happens-before序列(因此是第二种形式)。

谢谢你的解释。理论上我能够理解你所说的内容。我尝试使用一些程序来证明你所说的,但我无法做到。你能给我一个工作的例子吗?转义这个引用会产生什么影响? - John
在单线程情况下,可见性问题并不存在。它们只会在多个线程交错时显现出来。即使如此,要可靠地重现这些问题也不是那么容易。故障很少且随机。 - sjlee

2

无论何时您有一个非静态的内部类,例如

class Outer {
    class Inner {
    }
}

内部类有一个引用外部类的隐藏字段,你可以想象它是这样的

class Outer {
    class Inner {
        Outer hiddenOuter;
    }
}

因此,无论内部对象在哪里使用,外部对象都是可引用的,因此其生命周期至少与内部对象一样长。


《Java并发编程实战》这本书的标题似乎是指sjlee提到的原因,但这里还有另一个重要的观点:使用作为事件监听器的内部对象可以确保在内部对象从源中移除(或源本身被收集)之前,外部对象不会被垃圾回收。 - Paŭlo Ebermann
谢谢。我之前没有想过,现在我明白了。 - John

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接