【问题标题】:Non volatile double checked locking, is it possible?非易失性双重检查锁定,有可能吗?
【发布时间】:2014-08-22 08:57:02
【问题描述】:

这是我的单身课程。

静态instance 字段不是易失性的,因此会出现重新排序/可见性问题。为了解决这个问题,实例 val 字段是最终的。由于实例是正确构造的,因此如果他们看到实例,它的客户应该总是看到 val 字段已初始化。

    static class Singleton {
    private static Singleton instance;
    private final String val;
    public Singleton() { this.val = "foo"; }

    public static Singleton getInstance() {
        if (instance == null)
            synchronized (Singleton.class) {
                if(instance == null) {
                    instance = new Singleton();
            }
        }
        return instance;
    }
    public String toString() { return "Singleton: " + val; }
}

但是还有另一个问题 - 我有两个不受保护的“实例”字段读取,可以(?)重新排序,以便客户端可能会得到空值而不是实际值:

public static Singleton getInstance() {
    Singleton temp = instance;
    if (instance != null) return temp;
    else { /* init singleton and return instance*/ }
}

为了解决这个问题,我觉得我可以引入局部变量:

public static Singleton getInstance() {
    Singleton temp = instance;
    if (temp == null)
        synchronized (Singleton.class) {
            if(instance == null) {
                instance = new Singleton();
                temp = instance;
        }
    }
    return temp;
}

这似乎解决了问题,因为只有一个不受保护的值读取,所以不会发生真正的邪恶。但是......我刚刚修改了程序流程,而没有(几乎?)改变它的单线程语义。这是否意味着编译器可以撤消我的解决方法,因为这种转换是安全的,并且如果不与 volatile 建立适当的先发生关系,就无法使此代码正常工作?

【问题讨论】:

  • 从 Java 1.5 开始,单例实现的问题已经用枚举解决了:stackoverflow.com/a/8027815/32090
  • 这个问题实际上与单身人士无关。而是关于双重检查锁定本身。
  • 这些问题非常难以解决。这就是为什么我们应该使用 Java 从 java.util.concurrent 包中提供的抽象结构。

标签: java volatile java-memory-model double-checked-locking


【解决方案1】:

我不确定是否真的会发生对同一变量的读取重新排序,但可以保证局部变量不受其他线程活动的影响。即使没有发生这种读取重新排序,这种保证也适用于在您读取它时可能同时更新的每个变量:如果您读取一个值并将其存储到局部变量中,您可以确定局部变量的值确实之后不会突然改变。当然,如果值是引用,则该保证不适用于被引用对象的字段。

相关句子可以在JLS §17.4.1:

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

所以答案是否定的,不允许编译器撤消引入局部变量的解决方法。

【讨论】:

  • 啊,谢谢,这正是我想要的。
【解决方案2】:

进行惰性初始化单例最安全的方法是使用另一个类来保存单个实例字段并依赖 Java 语言为类初始化提供的保证

public class Singleton {
  private static class Holder {
    static final Singleton instance = new Singleton();
  }

  public Singleton getInstance() {
    return Holder.instance;
  }
}

Holder 类只会在第一次调用 getInstance() 时被初始化(并因此创建实例)。

【讨论】:

    【解决方案3】:

    我不认为你从一开始就有问题。

    您使用synchronized(Singleton.class)。在synchronized 上,Java 保证在此关键字之前的任何读/写操作都会很容易地反映到所涉及变量的内存中。由于您的 Singleton instance 也是在类级别声明的,因此对它的任何修改都可以从其他类中轻松看到并填充到主内存中。

    【讨论】:

    • 问题是在方法的第一行和最后一行读取值。这些读数在同步块之外,因此在读取和写入之间没有建立之前发生的关系。这就是为什么在没有 volatile 关键字的情况下双重检查锁定不起作用的原因。因此,我很确定问题存在。
    • 您的第一行只是一个 NULL 检查,并且实例只初始化了一次;因此,如果该线程很容易看到这一点,那就太好了;如果没有,您进入同步状态,仍然没有问题。不过,我看不到您将哪一行称为最后一行。
    • 据我了解重新排序规则非易失性读取可以重新排序。这就是为什么第一次读取可能返回非空值(检查失败)而第二次读取返回空值的原因。为了说明这一点,我放置了第二个 sn-p 代码 - 它演示了有效的重新排序(至少我认为它是有效的),它破坏了多线程应用程序(第一个线程从实例字段读取 null,然后第二个线程将实例设置为非 null,然后第一个线程从实例字段中读取非空值并返回存储在 temp 中的空值)。可能是我的推理是错误的,所以请随时纠正我。
    • 记住 2 点:1. 用 synchronized() 分隔的语句不会发生重新排序。 2. 在 synchronized() 时,所有涉及的变量都被刷新到主内存中,并确保值是最新的。所以你的第一个 null 第二个 null 情况永远不会发生。
    猜你喜欢
    • 2012-01-02
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2010-09-14
    • 2023-03-21
    • 1970-01-01
    • 2018-02-28
    相关资源
    最近更新 更多