非静态值的延迟初始化

时间:2014-08-13 15:10:17

标签: java concurrency double-checked-locking

这个问题实际上是指一个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;
      }

  }

}

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

3 个答案:

答案 0 :(得分:3)

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

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

但它只需要pre-Java 5(它不支持volatile的获取/发布语义),如果多个线程可能访问LazyEvaluator的同一个实例。如果每个线程都有一个线程本地实例,那么您不需要同步。

答案 1 :(得分:3)

最简单的解决方案是

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

因为LazyValue琐碎的对象,根本不值得记住。


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

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上)的出现与其实际需求完全不相称。

答案 2 :(得分:1)

好吧,“有效的替代懒惰初始化习惯用法”留下了很大的灵活性,所以我会把我的两分钱放在环中,注意这可能是应用库的好地方。特别是番石榴。 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类使用(正确)双重检查锁定。就成语而言,供应商肯定是有效且非常受欢迎的--java.util.function.Supplier来自Java 8。

祝你好运。