【问题标题】:Lazy-loaded singleton: Double-checked locking vs Initialization on demand holder idiom延迟加载的单例:双重检查锁定与按需初始化持有者成语
【发布时间】:2011-05-31 14:28:28
【问题描述】:

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

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

仅通过查看代码,Initialization on demand holder 惯用语似乎更简洁、更高效(但是,嘿,我猜这里)。尽管如此,我还是必须注意并记录每个单身人士的模式。至少对我来说,很难理解为什么代码会在现场写成这样……

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

替代品

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

【问题讨论】:

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


    【解决方案1】:

    添加另一个可能更简洁的选项。我建议使用枚举变体:

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

    【讨论】:

    • 不错。但我不确定我是否掌握了它的工作原理。假设Elvis.getAge() 需要一些非常密集的操作,我希望它们尽可能地延迟并且只执行一次。我应该把加载代码放在哪里?在枚举构造函数中?
    • @djg 听起来对我来说已经足够好了。但是有没有办法进一步延迟计算。例如,假设我想独立加载几个不同的资源。使用这种模式,我将不得不编写不同的枚举。有没有办法只用一个枚举来实现这一点?假设ElvisINSTANCE1.getAge()INSTANCE2.getAge()。我可以让它们以线程安全的方式独立加载吗?
    • 对。我在考虑单独的枚举/单例。如果您有多个“实例”,那么您的构造函数会为每个调用一次。据我了解,这是不可取的。
    • @djg。我想我没有很好地表达自己。我的意思是。如果我想让INSTANCE1.getAge() 延迟加载一个值而INSTANCE2.getAge() 延迟加载另一个值怎么办?在这两种情况下,他们都应该只加载它的值一次(并且单独加载)。是否有可能实现这种行为? (多个枚举很好,我只是想知道它是否可以改进)。
    • 如果您正在处理方法调用中值的延迟加载(和同步),那么您正在规避枚举单例的好处。我不确定您的所有要求,但我会建议您每个单独的单例枚举类都将实现的接口。或者只是一个包含所有资源的单例(假设您可以在加载时处理“一次性”性能)。
    【解决方案2】:

    就可读性而言,我会选择按需初始化。我觉得双重检查锁定是一个过时且丑陋的实现。

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

    【讨论】:

      【解决方案3】:

      Initialisation-on-demand holder 仅适用于单例,您不能拥有每个实例延迟加载的元素。双重检查锁定给必须查看课程的每个人带来了认知负担,因为很容易以微妙的方式出错。在我们将模式封装到 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 策略的超时。

      【讨论】:

      • 有时称为虚拟代理模式...该实现很好地说明了“雷霆群”问题的解决方案,即多个调用者请求相同的资源,但您只想获取一次资源。
      【解决方案4】:

      Initialization-on-demand holder 始终是实现单例模式的最佳实践。它很好地利用了JVM的以下特性。

      1. 静态嵌套类仅在按名称调用时加载。
      2. 默认情况下,类加载机制受并发保护。所以当一个线程初始化一个类时,其他线程等待它的完成。

      另外,您不必使用 synchronize 关键字,它会使您的程序慢 100 倍。

      【讨论】:

        【解决方案5】:

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

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

        【讨论】:

        • 那么,就速度而言:按需初始化>双重检查锁定>同步方法对吗?我也是这么想的。也许是时候进行一些邪恶的微基准测试了。
        • @Anthony,没错。基本上,需求持有者在没有不稳定负载的情况下实现了您希望 DCL 执行的操作。
        • @Anthony - 微基准测试并不邪恶。很难获得有意义且适用于您的实际用例的结果。
        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2016-06-03
        • 2014-11-25
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多