Java,懒惰地初始化字段而没有同步

时间:2011-04-06 14:14:37

标签: java synchronization lazy-evaluation

有时当我需要懒惰的初始化字段时,我使用以下设计模式。

class DictionaryHolder {
  private volatile Dictionary dict; // some heavy object

  public Dictionary getDictionary() {
    Dictionary d = this.dict;
    if (d == null) {
      d = loadDictionary(); // costy operation
      this.dict = d;
    }
    return d;
  }
}

看起来像Double Checking idion,但不完全是。没有同步,可以多次调用loadDictionary方法。

当并发性很低时,我使用这种模式。在使用这种模式时,我还要记住以下假设:

  • loadDictionary方法始终返回相同的数据。
  • loadDictionary方法是线程安全的。

我的问题:

  1. 这种模式是否正确?换句话说,getDictionary()是否可以返回无效数据?
  2. 是否可以使dict字段非易失性以提高效率?
  3. 有没有更好的解决方案?

7 个答案:

答案 0 :(得分:3)

最简单的解决方案是依赖于在需要之前不加载类的事实。即无论如何它都是懒惰的。这样你就可以避免自己做这些检查。

public enum Dictionary {
 INSTANCE;

  private Dictionary() {
     // load dictionary
  }
}

不应该让它变得更复杂,当然你也不会让它变得更有效率。

编辑:如果Dictionary需要扩展List或Map,你可以这样做。

public enum Dictionary implements List<String> { }

或者更好的方法是使用字段。

public enum Dictionary {
    INSTANCE;
    public final List<String> list = new ArrayList<String>();
}

或使用静态初始化块

public class Dictionary extends ArrayList<String> {
    public static final Dictionary INSTANCE = new Dictionary();
    private Dictionary() { }
}

答案 1 :(得分:3)

我个人觉得Initialization on demand holder idiom非常适合这种情况。来自维基:

public class Something {

        private Something() {}

        private static class LazyHolder {
                private static final Something INSTANCE = new Something();
        }

        public static final Something getInstance() {
                return LazyHolder.INSTANCE;
        }
}

虽然这可能看起来像纯粹用于单例控制的模式,但你可以用它做更多很酷的事情。对于例如holder类可以调用一个方法,该方法反过来填充某种数据。

此外,在您的情况下,如果多个线程在loadDictionary调用(已同步)上排队,您可能最终会多次加载相同的事件。

答案 2 :(得分:2)

  

1.这种模式是否正确?换句话说,getDictionary()是否可以返回无效数据?

是的,如果可以同时由多个线程调用loadDictionary(),那么对getDictionary()的不同调用可以返回不同的对象。否则,您需要一个具有同步功能的解决方案。

  

2.是否可以使dict字段非易失性以提高效率?

不,它可能导致内存可见性问题。

  

3.有没有更好的解决方案?

只要你想要一个没有同步的解决方案(显式或隐式) - 没有(据我所知)。否则,有许多习语,例如使用enum或内部持有者类(但它们使用隐式同步)。

答案 3 :(得分:2)

快速刺破这个但是......

class DictionaryHolder {
  private volatile Dictionary dict; // some heavy object

  public Dictionary getDictionary() {
    Dictionary d = this.dict;
    if (d == null) {
      synchronized (this) {
        d = this.dict;
        if (d == null) { // gated test for null
          this.dict = d = loadDictionary(); // costy operation
        }
    }
    return d;
  }
}

答案 4 :(得分:2)

您的代码是正确的。为避免加载多次,synchronized{}会很好。

如果volatile是不可变的,您可以删除Dictionary

private Dictionary dict; // not volatile; assume Dictionary immutable

public Dictionary getDict()
    if(dict==null)
        dict = load()
    return dict;

如果我们添加双重检查锁定,那就完美了

public Dictionary getDict()
    if(dict==null)
        synchronized(this)
             if(dict==null)
                  dict = load()
    return dict;

双重检查锁定对于不可变对象非常有用,无需使用volatile。


不幸的是,上述2 getDict()方法在理论上并不是防弹的。弱java内存模型将允许一些怪异的行为 - 理论上。要使本书100%正确,我们必须添加一个局部变量,这会使我们的代码混乱:

public Dictionary getDict()
    Dictionary local = dict;
    if(local==null)
        synchronized(this)
             local = dict;
             if(local==null)
                  local = dict = load()
    return local;

答案 5 :(得分:1)

  

是否可以使dict字段非易失性以提高效率?

没有。这会损害可见性,即当一个线程初始化dict时,其他线程可能无法及时(或根本)看到更新的引用。这反过来会导致多次重度初始化,因此很多无用的工作,更不用说返回对多个不同对象的引用

无论如何,在处理并发时,微观优化效率将是我最后的想法。

答案 6 :(得分:0)

按需初始化持有人类习惯用法

  

此方法仅依赖于JVM   将班级成员初始化   首先提到这个班级。在这   case,我们有一个内部类   仅在内部引用   getDictionary()方法。这意味着   DictionaryHolder将被初始化   在第一次调用getDictionary()时。

public class DictionaryHolder {

  private DictionaryHolder ()
  {
  }

  public static Dictionary getDictionary() 
  {
     return DictionaryLazyHolder.instance;
  }

  private static class DictionaryLazyHolder
  {
    static final DictionaryHolder instance = new DictionaryHolder();
  }
}