延迟加载的单例:双重检查锁定与初始化按需持有者习惯用法

时间:2011-05-31 14:28:28

标签: java concurrency singleton lazy-loading design-patterns

我要求在并发环境中延迟加载资源。加载资源的代码只能执行一次。

Double-checked locking(使用JRE 5+和volatile关键字)和Initialization on demand holder idiom似乎都很适合这项工作。

通过查看代码,初始化按需持有者习惯看起来更干净,更有效(但是,嘿,我在这里猜测)。不过,我必须小心并记录我的每一个单身人士的模式。至少在我看来,很难理解为什么代码就是这样写的......

我的问题是:哪种方法更好?为什么? 如果你的答案是否定的。您将如何在Java SE环境中解决此要求?

替代

我可以使用CDI而不强制它在整个项目中使用吗?那里有文章吗?

5 个答案:

答案 0 :(得分:7)

添加另一个,也许更干净的选项。我建议使用枚举变体:

What is the best approach for using an Enum as a singleton in Java?

答案 1 :(得分:7)

就可读性而言,我会选择初始化按需持有者。我认为,双重检查锁定是一种过时的和丑陋的实现。

从技术上讲,通过选择双重检查锁定,您将始终在字段上产生易失性读取,您可以使用初始化按需持有者惯用法进行正常读取。

答案 2 :(得分:7)

按需初始化持有者仅适用于单身人士,您不能拥有每个实例延迟加载的元素。双重检查锁定会给每个必须看课程的人带来认知负担,因为它很容易以微妙的方式出错。我们曾经遇到过各种麻烦,直到我们将模式封装到our concurrency library

中的实用程序类中

我们有以下选择:

Supplier<ExpensiveThing> t1 = new LazyReference<ExpensiveThing>() {
  protected ExpensiveThing create() {
    … // expensive initialisation
  }
};

Supplier<ExpensiveThing> t2 = Lazy.supplier(new Supplier<ExpensiveThing>() {
  public ExpensiveThing get() {
    … // expensive initialisation
  }
});

就使用而言,两者都具有相同的语义。第二种形式使内部供应商使用的任何参考资料在初始化后可用于GC。第二种形式也支持TTL / TTI策略的超时。

答案 3 :(得分:5)

初始化按需持有者始终是实施单例模式的最佳实践。它很好地利用了JVM的以下功能。

  1. 仅在按名称调用时才加载静态嵌套类。
  2. 默认情况下,类加载机制受并发保护。因此,当一个线程初始化一个类时,其他线程会等待它完成。
  3. 此外,您不必使用synchronize关键字,它会使您的程序慢100倍。

答案 4 :(得分:3)

我怀疑按需定时器的初始化速度略快于双重检查锁定(使用易失性)。原因是前者在创建实例后没有同步开销,但后者涉及读取一个volatile(我认为)需要完整的内存读取。

如果性能不是一个重要问题,那么同步getInstance()方法是最简单的。