【问题标题】:Lazy initialization for non-static values非静态值的延迟初始化
【发布时间】:2014-08-15 17:05:24
【问题描述】:

这个问题实际上是指different question,因为它可能没有很好地表述,所以被关闭为重复。

对于此代码示例(在多线程环境中),什么是有效的替代惰性初始化习惯用法而不是双重检查锁定:

public class LazyEvaluator {
  private final Object state;
  private volatile LazyValue lazyValue;

  public LazyEvaluator(Object state) {
      this.state = state;
  }

  public LazyValue getLazyValue() {
      if (lazyValue == null) {
          synchronized (this) {
              if (lazyValue == null) {
                  lazyValue = new LazyValue(someSlowOperationWith(state), 42);
              }
          }  
      }
      return lazyValue;
  }

  public static class LazyValue {
      private String name;
      private int value;

      private LazyValue(String name, int value) {
          this.name = name;
          this.value = value;  
      }

      private String getName() {
          return name;
      }

      private int getValue() {
          return value;
      }

  }

}

编辑已更新以包含缓慢的操作并添加了关于多线程环境的明确提及

【问题讨论】:

  • 您认为这“无效”的原因是什么?
  • 是的。问题是关于替代的,因为 DCL 通常被认为是反模式(参见参考问题)

标签: java concurrency double-checked-locking


【解决方案1】:

如果我理解你,那么你可以改变这个

public LazyValue getLazyValue() {
  if (lazyValue == null) {
    synchronized (this) {
      if (lazyValue == null) {
        lazyValue = new LazyValue(state.toString());
      }
    }  
  }
  return lazyValue;
}

到这里

public synchronized LazyValue getLazyValue() {
  if (lazyValue == null) {
    lazyValue = new LazyValue(state.toString());
  }
  return lazyValue;
}

但这只是 Java 5 之前的版本(它不支持 volatile 的获取/释放语义)以及多个线程可能访问您的 LazyEvaluator 的同一实例时才需要。如果每个线程都有一个线程本地实例,那么您不需要同步。

【讨论】:

  • 刚刚提到多线程环境。 synchronized 的问题在于,即使我们已经初始化了 LazyValue,它也可能成为争用的瓶颈,这就是使用双重检查锁定的原因。
【解决方案2】:

最简单的解决方案是

public LazyValue getLazyValue() {
    return new LazyValue(state.toString(), 42);
}

因为LazyValue 是一个微不足道的对象,根本不值得记住。


如果涉及昂贵的计算,您可以通过声明其字段 finalLazyValue 转换为真正的不可变对象:

public static class LazyValue {
    private final String name;
    private final int value;
// …

这样,即使通过数据竞争,您也可以发布实例:

// with lazyValue even not being volatile
public LazyValue getLazyValue() {
    return lazyValue!=null? lazyValue:
        (lazyValue=new LazyValue(state.toString(), 42));
}

在这种情况下,值可能会被计算多次,在不太可能的情况下,多个线程同时访问它,但一旦线程看到非null 值,它将是一个正确初始化的值由于final 字段初始化保证。


如果计算如此开销太大,甚至必须避免不太可能的并发计算,那么只需声明getLazyValue()synchronized,因为与将要保存的计算相比,它的开销可以忽略不计。


最后,如果您真的遇到计算量如此之大的情况,必须不惜一切代价避免重叠并发计算,但分析表明稍后同步是一个瓶颈,您可能遇到了一种非常罕见的情况双重检查锁定可能是一种选择(真的很少见)。

在这种情况下,您的问题代码仍有替代方案。将 DCL 与我上面的建议结合起来,将所有 LazyValue 的字段声明为 final,并使 lazyValue 持有人字段非volatile。这样,您甚至可以在构造惰性值之后保存 volatile 读取。但是,我还是要说,它应该真的很少需要。

也许这就是 DCL 享有如此多负面声誉的非技术原因:它出现在讨论中(或 StackOverflow 上)与其实际需求完全不成比例。

【讨论】:

  • LazyValue 可能微不足道,但难以计算——这就是它变得“懒惰”的原因。
  • 我同意可以忽略不计的开销,但仅限于第一次调用。更多的将比他们应该的慢几倍。
  • @AngryJuice:你有什么“比他们应该慢几倍”的证明吗?同步的成本经常被高估。
  • 很难给出准确的估计,但举个例子:cs.umd.edu/~pugh/java/memoryModel/DCL-performance.html
  • @AngryJuice:你错了,这些值已经过时了。它们比我们正在谈论的 Java 内存模型更老。在这些 JVM 上,即使是使用 volatile 的“固定 DCL”也不能可靠地工作。顺便说一句,您在 Doug Lea 和 Bill Pugh 的这些页面上找到的所有内容都已被纳入今天的 JVM 开发,因为这两个人是我们今天所知的 JMM 的主要负责人……
【解决方案3】:

好吧,“有效的替代惰性初始化习语”留下了很大的灵活性,所以我将把我的两分钱放在戒指上,指出这可能是应用库的好地方。特别是番石榴。 https://code.google.com/p/guava-libraries/

// You have some long method call you want to make lazy
MyValue someLongMethod(int input) { ... }
// So you wrap it in a supplier so it's lazy
Supplier<MyValue> lazy = new Supplier<MyValue>() { 
  public MyValue get() {
    return someLongMethod(2);
  }
}
// and you want it only to be called once ...
Supplier<MyValue> cached = Suppliers.memoize(lazy);
// ... and someLongMethod won't actually be called until
cached.get();

Suppliers 类(正确地)使用了双重检查锁定。就成语而言,Supplier 确实有效且非常流行——java.util.function.Supplier 来自 Java 8。

祝你好运。

【讨论】:

    猜你喜欢
    • 2011-11-17
    • 1970-01-01
    • 1970-01-01
    • 2023-04-04
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多