Java:引用转义

时间:2010-09-14 00:49:57

标签: java concurrent-programming

请阅读以下代码是“不安全构造”的示例,因为它允许此引用转义。我无法理解'这个'是如何逃脱的。我是java世界的新手。任何人都可以帮助我理解这一点。

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

4 个答案:

答案 0 :(得分:23)

您在问题中发布的示例来自Brian Goetz等人的"Java Concurrency In Practice"。它在第3.2节“出版和逃避”中。我不会在这里尝试重现该部分的细节。 (去买你书架的副本,或者从你的同事那里借一份!)

示例代码说明的问题是构造函数允许在构造函数完成创建对象之前对正在构造的对象的引用进行“转义”。这是一个问题,原因有两个:

  1. 如果引用转义,则可以在构造函数完成初始化之前使用该对象,并在不一致(部分初始化)状态下查看它。即使对象在初始化完成后转义,声明子类也可能导致违反此规则。

  2. 根据JLS 17.5,可以安全地使用对象的最终属性而无需同步。但是,仅当对象引用在构造函数完成之前未发布(不转义)时才会出现这种情况。如果违反此规则,结果就是一个阴险的并发错误,当代码在多核/多处理器计算机上执行时,可能咬你。

  3. ThisEscape示例是偷偷摸摸的,因为引用是通过隐式传递给匿名this类构造函数的EventListener引用转义的。但是,如果过早明确发布参考文献,也会出现同样的问题。

    以下是一个示例来说明未完全初始化对象的问题:

    public class Thing {
        public Thing (Leaker leaker) {
            leaker.leak(this);
        }
    }
    
    public class NamedThing  extends Thing {
        private String name;
    
        public NamedThing (Leaker leaker, String name) {
            super(leaker);
    
        }
    
        public String getName() {
            return name; 
        }
    }
    

    如果Leaker.leak(...)方法在泄漏的对象上调用getName(),它将获得null ...因为在那个时间点对象的构造函数链尚未完成。

    以下是一个示例,用于说明final属性的不安全发布问题。

    public class Unsafe {
        public final int foo = 42;
        public Unsafe(Unsafe[] leak) {
            leak[0] = this;   // Unsafe publication
            // Make the "window of vulnerability" large
            for (long l = 0; l < /* very large */ ; l++) {
                ...
            }
        }
    }
    
    public class Main {
        public static void main(String[] args) {
            final Unsafe[] leak = new Unsafe[1];
            new Thread(new Runnable() {
                public void run() {
                    Thread.yield();   // (or sleep for a bit)
                    new Unsafe(leak);
                }
            }).start();
    
            while (true) {
                if (leak[0] != null) {
                    if (leak[0].foo == 42) {
                        System.err.println("OK");
                    } else {
                        System.err.println("OUCH!");
                    }
                    System.exit(0);
                }
            }
        }
    }
    

    此应用程序的某些运行可能打印“OUCH!”而不是“OK”,表示由于通过Unsafe数组的不安全发布,主线程已将leak对象视为处于“不可能”状态。是否发生这种情况取决于您的JVM和硬件平台。

    现在这个例子显然是人为的,但不难想象在真正的多线程应用程序中这种事情会发生什么。


    由于JSR 133,Java 5(JLS的第3版)中指定了当前的Java内存模型。在此之前,Java的内存相关方面未得到充分说明。引用早期版本/版本的来源已过时,但Goetz第1版中有关内存模型的信息是最新的。

    内存模型的某些技术方面显然需要修改;见https://openjdk.java.net/jeps/188https://www.infoq.com/articles/The-OpenJDK9-Revised-Java-Memory-Model/。但是,这项工作尚未出现在JLS修订版中。

答案 1 :(得分:13)

我有同样的怀疑。

问题在于,在其他类中实例化的每个类都引用了变量$this中的封闭类。

这就是java所谓的合成,它不是你定义的东西,而是java自动为你做的事情。

如果您想亲眼看到这一点,请在doSomething(e)行中添加一个断点,并检查EventListener有哪些属性。

答案 2 :(得分:5)

我的猜测是在doSomething类中声明了ThisEscape方法,在这种情况下,引用肯定可以“逃避”。
即,某些事件可以在创建EventListener后立即触发此ThisEscape,并且在ThisEscape构造函数的执行完成之前。反过来,监听器将调用var的实例方法。

我会稍微修改你的例子。现在,变量doSomething可以在public class ThisEscape { private final int var; public ThisEscape(EventSource source) { source.registerListener( new EventListener() { public void onEvent(Event e) { doSomething(e); } } ); // more initialization // ... var = 10; } // result can be 0 or 10 int doSomething(Event e) { return var; } } 方法中访问,然后才能在构造函数中分配。

{{1}}

答案 3 :(得分:3)

我在阅读Brian Goetz的“Java Concurrency In Practice”时遇到了完全相同的问题。

Stephen C 的回答(被接受者)非常好!我只想添加一个我发现的资源。它来自 JavaSpecialists ,Heinz M. Kabutz博士正好分析了 devnull 发布的代码示例。他解释了编译后生成的类(外部,内部)以及this如何转义。我发现这个解释很有用所以我觉得分享:)

issue192(他扩展了示例并提供了竞争条件。)

issue192b(他解释了编译后生成了什么类,以及this如何逃避。)