【问题标题】:Can anonymous classes be completely immutable?匿名类可以完全不可变吗?
【发布时间】:2021-06-13 13:28:03
【问题描述】:

在 Java Concurrency In Practice 一书中,有一个几乎不可变对象的示例,如果没有正确发布,它就有失败的风险:

// Taken from Java Concurrency In Practice
// p.51 Listing 3.15: Class at risk of failure if not properly published.
public class Holder {
    private int n;

    public Holder(int n) { this.n = n; }

    public void assertSanity() {
        if(n != n)
            throw new AssertionError("This statement is false.");
    }
}

// p.50 Listing 3.14: Publishing an object without adequate synchronization. Don't do this.
class Client {
    public Holder holder;

    public void initialize() {
        holder = new Holder(42);
    }
}

如果我正确理解书中的章节,将final 添加到Holder 类的n 字段将使对象完全不可变,并且即使AssertionError 仍然存在,也不会被抛出在没有充分同步的情况下发布,就像在 Client 类中所做的那样。

现在我想知道匿名类在这方面的表现如何。请看下面的例子:

public interface IHolder {
    void assertSanity();
}

class IHolderFactory {
    static IHolder create(int n) {
        return new IHolder() {
            @Override
            public void assertSanity() {
                if (n != n)
                    throw new AssertionError("This statement is false.");
            }
        };
    }
}

class IHolderClient {
    public IHolder holder;

    public void initialize() {
        // is this safe?
        holder = IHolderFactory.create(42);
    }
}

就像书中的例子一样,它在没有充分同步的情况下发布,但不同的是,现在Holder 类已成为一个接口,并且有一个静态工厂方法返回一个实现该接口的匿名类,而匿名类使用方法参数n

我的问题是:有没有机会从我的后一个示例中获得AssertionError?如果有,使其完全不可变并消除问题的最佳方法是什么?如果它以如下的函数方式编写,它会改变什么吗?

class IHolderFactory {
    static IHolder create(int n) {
        return () -> {
            if (n != n)
                throw new AssertionError("This statement is false.");
        };
    }
}

【问题讨论】:

    标签: java multithreading concurrency language-lawyer


    【解决方案1】:

    这是一个非常棘手的问题。

    JLS, §17.4.1. Shared Variables 说:

    局部变量 (§14.4)、形式方法参数 (§8.4.1) 和异常处理程序参数 (§14.20) 绝不会在线程之间共享,并且不受内存模型的影响。

    这似乎与您可以在可以在线程之间共享的内部类或 lambda 表达式中使用它们的事实相矛盾,但是这些构造捕获变量的值并使用该值。然而,这个过程并没有得到很好的说明。

    我能找到的唯一提及是 §15.27.2 解释(有效)最终要求:

    对有效最终变量的限制禁止访问动态变化的局部变量,这些变量的捕获可能会引入并发问题。

    在实践中,捕获的值存储在内部类的合成 final 字段中或在运行时为 lambda 表达式生成的类中。所以你永远不会看到当前实现的错误。

    然而,这并没有在任何地方指定。语言规范说little about the bytecode format,而虚拟机规范几乎没有提到语言结构。

    因此,局部变量、形式方法参数和异常处理程序参数被明确排除在 JMM 之外,并且它们捕获的值在 JMM 方面不是变量,甚至没有被提及。问题是这意味着什么。

    它们通常不受数据竞争的影响(我的解释)还是它们不安全并且我们根本没有从 JMM 获得任何保证?在后一种情况下,它甚至暗示我们无法保证它们的安全,因为任何安全的发布机制都可以从不包括我们的情况的 JMM 保证中获得安全性。值得注意的是,JMM 也没有覆盖外部 this 引用,也没有覆盖实例对 getClass() 返回的 Class 对象的隐式引用。

    因此,虽然我认为它们不受数据竞争的影响,但我希望更明确地说明这一点。

    【讨论】:

      【解决方案2】:

      不管你是使用匿名类还是 lambda,你都有同步机制来正确发布引用;因此,此代码可以抛出该异常。

      换句话说,您必须满足一些工具和条件才能使您的代码安全:这些工具和条件使用finalvolatile 或某种synchronizedlocks 等。因为您使用无,不提供任何保证。

      Java 语言规范在您使用特殊语义时提供这些保证,例如您在第一个示例中展示的final。还有其他方法,但使对象不可变是最简单、最简单的方法。 This is the best article 我知道关于这个主题,您可能想阅读它。

      【讨论】:

      • 我不确定这是不是“最好的文章”。它首先介绍了作者称之为“安全发布”和“安全初始化”的两个概念,然后立即开始处理单例,这是不正确内存和并发语义的典型代表。单例被认为是反模式的部分原因是很难正确编写它们。
      • @RobertHarvey 你需要向下滚动到safe publicationsafe initialization
      • 我已经这样做了。我的观点是,您可以深入研究这些语义(从学术角度来看这可能是个好主意),或者您可以简单地避免需要对 Java 的内存模型进行这种深入理解的情况。
      • @KoheiNozaki 不,它没有。
      • 你知道,规范并不保证 lambda 表达式会产生一个新对象。所以原则上,它可能导致一个完全不同的线程先前创建的对象,并且规范没有说这会在未知创建者和当前线程之间建立 happens-before 关系。如果我们不假设 lambda 表达式本质上是不可变的,那么祝您在这种情况下解释如何使发布线程安全。
      猜你喜欢
      • 2020-06-15
      • 1970-01-01
      • 2010-09-16
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2014-04-03
      • 2016-04-24
      相关资源
      最近更新 更多