【问题标题】:Java: reference escapeJava:引用转义
【发布时间】:2010-09-14 00:49:57
【问题描述】:

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

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

【问题讨论】:

    标签: java concurrent-programming


    【解决方案1】:

    您在问题中发布的示例来自 Brian Goetz 等人的 "Java Concurrency In Practice"。它在第 3.2 节“发布和转义”中。我不会尝试在此处重现该部分的详细信息。 (去为你的书架买一本,或者从你的同事那里借一本!)

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

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

    2. 根据JLS 17.5,对象的最终属性可以在不同步的情况下安全使用。但是,仅当对象引用在其构造函数完成之前未发布(不转义)时才适用。如果您违反此规则,结果是一个潜在的并发错误,当代码在多核/多处理器机器上执行时,可能会困扰您。

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

    这里举个例子来说明对象初始化不完全的问题:

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

    此应用程序的某些运行可能打印“哎哟!”而不是“OK”,表示由于通过leak 数组的不安全发布,主线程已经观察到Unsafe 对象处于“不可能”状态。这是否发生取决于您的 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 修订版中。

    【讨论】:

    • 那么这本质上是 JVM 内存模型中的“错误”吗?
    • 那么ibm.com/developerworks/java/library/j-jtp0618.html 有多少已经过时了?
    • Stephen:Goetz 说“JMM 正在 Java Community Process JSR 133 下进行修订,这将(除其他外)改变 volatile 和 final 的语义,使它们更符合一般直觉。”那是 2002 年写的。过去 8 年发生了多大的变化?
    • @Gabe - 由于 JSR 133,内存模型从 JLS 2 更改为 JLS 3。要查看差异,请比较 JLS 第 2 版和第 3 版的第 17 章。 AFAIK,没有任何更改使 Goetz 2002 年的文章无效。他的书在 2006 年进行了修订,描述了 JLS 第 3 版内存模型。自 JLS 第 3 版/Java 5.0 以来,内存模型没有改变。
    • (“自 JLS 第 3 版/Java 5.0 以来,内存模型没有改变。” - 已对 规范 的内存模型来处理专家指出的各种问题……但意图并没有改变。)
    【解决方案2】:

    我也有同样的疑问。

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

    这就是java所说的synthetic,它不是你定义的东西,而是java自动为你做的东西。

    如果您想亲自查看此内容,请在 doSomething(e) 行中设置断点并检查 EventListener 具有哪些属性。

    【讨论】:

    • 查看我的答案以获得真正的解释。
    • 我认为这是问题的重要部分;它准确地解释了 如何 this 转义,这就是问题所要问的(为什么 转义不好不是问题的明确部分)。
    【解决方案3】:

    我的猜测是doSomething 方法是在ThisEscape 类中声明的,在这种情况下,引用肯定可以“转义”。
    即,某些事件可以在它创建之后和ThisEscape 构造函数执行完成之前触发这个EventListener。而监听器又会调用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;
        }
    }
    

    【讨论】:

    • 这不是最快乐的例子,因为出自这个问题的书(Concurrency in Practice)说即使 registerListener 是构造函数中的最后一行,构造不佳的对象仍然可以逃脱跨度>
    • @Pablo 怎么样? (我不是在挑战这本书,只是对“失败”的例子感到好奇)
    • 我也是,我认为这与外部对象在onEvent 方法中可见的事实有关。并且 java 不保证正确的变量初始化,除非字段被声明为 final (这也来自那本书)。注意:我还没有读完:P
    • 希望 Jon Skeet 能很快介入并揭开谜团 :) (我们也投了赞成票)
    • @StephenC 您的解释是关于明确的 this 引用,其中很明显 this 逃脱了。但是,在本书的示例中,没有明确提到“this”。我认为 Nikita 遇到了问题:this 引用被“隐藏”转义,不幸的是,在 ThisEscape 的构造函数能够完成之前,外部对象能够(间接)调用 doSomething()。这就是本书使用 EventSource 和 listeners 示例的原因。
    【解决方案4】:

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

    Stephen C 的答案(已被接受)非常好!我只想在我发现的那个资源之上再添加一个。它来自 JavaSpecialists,Heinz M. Kabutz 博士在其中准确分析了 devnull 发布的代码示例。他解释了编译后生成了哪些类(外部、内部)以及this 如何转义。我发现这个解释很有用,所以我想分享:)

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

    issue192b(他在这里解释了编译后会生成什么样的类以及this如何转义。)

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2013-07-06
      • 1970-01-01
      • 2011-09-10
      • 1970-01-01
      • 1970-01-01
      • 2012-12-31
      相关资源
      最近更新 更多