【问题标题】:Does Java synchronized keyword flush the cache?Java synchronized 关键字是否刷新缓存?
【发布时间】:2010-08-30 14:58:29
【问题描述】:

仅限 Java 5 及更高版本。假设一台多处理器共享内存计算机(您现在可能正在使用一台)。

下面是单例延迟初始化的代码:

public final class MySingleton {
  private static MySingleton instance = null;
  private MySingleton() { } 
  public static MySingleton getInstance() {
    if (instance == null) {
      synchronized (MySingleton.class) {
        if (instance == null) {
          instance = new MySingleton();
        }
      }
    }
    return instance;
  }
}

是否必须将 instance 声明为 volatile 以防止优化器重写 getInstance() 如下(这在顺序程序中是正确的):

public static MySingleton getInstance() {
  if (instance == null) {
    synchronized (MySingleton.class) {
      // instance must be null or we wouldn't be here  (WRONG!)
      instance = new MySingleton();
    }
  }
}

假设优化器不重写代码,如果instance没有声明volatile是否仍然保证在退出synchronized块时被刷新到内存,并在synchronized块时从内存中读取输入了吗?

编辑:我忘记将 getInstance() 设为静态。我认为这不会改变答案的有效性;你们都知道我的意思。

【问题讨论】:

  • 提示:您可以使用javap -c com.example.ClassName 反汇编代码,以便检查它是如何编译的,以及编译器是否更改/优化了其中一个。我用 JDK 1.6 之一为你检查了它,编译器没有去掉嵌套的if
  • 使用 javap 很有趣,但 javac 中的优化器通常很差。故意。真正的优化器在运行时的 JIT 编译器中。
  • 我很抱歉只将其中一个答案选为“答案”。到目前为止,所有这些都值得检查,但它只能让我检查一个。
  • volatile 不是必要的。公众对双重检查锁定的了解已经过时了。
  • 信誉不佳,能解释一下吗?

标签: java volatile synchronized double-checked-locking


【解决方案1】:

是的,instance 应该声明为 volatile。即使这样,也建议不要使用双重检查锁定。它(或者准确地说,Java 内存模型)曾经有一个严重的缺陷,允许发布部分实现的对象。这已在 Java5 中修复,但 DCL 仍然是一个过时的习惯用法,不再需要使用它 - 请改用 lazy initialization holder idiom

来自Java Concurrency in Practice,第 16.2 节:

DCL 的真正问题是假设在没有同步的情况下读取共享对象引用时可能发生的最糟糕的事情是错误地看到一个陈旧的值(在这种情况下,null);在这种情况下,DCL 习惯用法通过在持有锁的情况下再次尝试来补偿这种风险。但最坏的情况实际上要糟糕得多 - 可能会看到引用的当前值但对象状态的陈旧值,这意味着可以看到对象处于无效或不正确的状态。

JMM(Java 5.0 及更高版本)中的后续更改使 DCL 能够工作如果 resource 变为 volatile,并且由于 volatile 读取是通常只比非易失性读取稍微贵一点。然而,这是一个实用性已基本消失的习语——推动它的力量(缓慢的非竞争同步、缓慢的 JVM 启动)不再起作用,使其作为优化的效果降低。惰性初始化持有者习语提供相同的好处并且更容易理解。

【讨论】:

  • +1 非常有趣。我总是被 *er 智取。这是一个永无止境的谦卑练习。
  • 如果 Edsger Dijkstra 还在我们身边,他可能会说,如果专家级程序员总是被 * 智取,那么这个行业就处于糟糕的状态。
  • 持有者类成语只适用于单身人士。不是惰性初始化的一般机制。
  • @irrep,是的,但是这个问题是关于单例的,而不是一般的惰性初始化。
【解决方案2】:

是的,实例需要在 Java 中使用双重检查锁定是易失的,否则 MySingleton 的初始化可能会将部分构造的对象暴露给系统的其余部分。线程在到达“已同步”语句时也会同步,这也是事实,但在这种情况下为时已晚。

Wikipedia 和其他几个 Stack Overflow 问题对“双重检查锁定”进行了很好的讨论,所以我建议您阅读一下。我还建议不要使用它,除非性能分析表明此特定代码确实需要性能。

【讨论】:

  • (来自 javaworld 文章)通常建议的非修复方法是将 SomeClass 的资源字段声明为 volatile。然而,虽然 JMM 防止对 volatile 变量的写入相对于彼此重新排序并确保它们立即刷新到主存储器,但它仍然允许对 volatile 变量的读取和写入相对于非易失性读取和写入重新排序。这意味着 - 除非所有 Resource 字段也是 volatile 的 - 线程 B 仍然可以感知构造函数的效果,因为在将资源设置为引用新创建的资源之后发生。
  • 这在 Java5 中已得到修复,但值得指出的是,在较旧的 VM 中,即使 volatile 也无法解决问题。
【解决方案3】:

对于 Java 单例的延迟初始化有一个更安全、更易读的习惯用法:

class Singleton {

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

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

  private Singleton create() {
    ⋮
  }

  private Singleton() { }

}

如果您使用更详细的双重检查锁定模式,您必须声明字段volatile,正如其他人已经指出的那样。

【讨论】:

    【解决方案4】:
    【解决方案5】:

    没有。它不一定是易变的。 见How to solve the "Double-Checked Locking is Broken" Declaration in Java?

    以前的尝试都失败了,因为如果你可以欺骗 java 并避免 volatile/sync,java 可以欺骗你并给你一个不正确的对象视图。然而新的final 语义解决了这个问题。如果你得到一个对象引用(通过普通读取),你可以安全地读取它的final 字段。

    【讨论】:

    • 不过,惰性初始化持有者习惯用法以更清晰的方式实现了相同的效果,代码更少(因此出错的机会更少)并且不需要显式同步。
    • 只适用于单身人士。不是为了在运行时懒惰地创建动态数量的对象。
    • 这个问题专门针对单身人士。
    最近更新 更多