多线程:惰性初始化 vs 静态惰性初始化

13

我正在观看Java内存模型视频演示,作者说与懒加载相比,使用静态延迟初始化更好,但我不太清楚他想表达什么。

我想向社区寻求帮助,并希望有人能用简单的Java代码例子来解释静态延迟初始化懒加载之间的区别。

参考资料:高级编程主题 - Java内存模型


4
这段视频时长57分钟。虽然很有趣,但你也许可以告诉我们在视频的哪个时间点讨论了这个特定的话题。 - JB Nizet
2
请从第43分钟到第48分钟观看,这5分钟讨论了静态延迟初始化。 - Rachel
6个回答

22

首先,这两种实现方式都可以是静态的,这就是第一个误解。视频中的演讲者正在解释如何利用类初始化的线程安全性。

类初始化本质上是线程安全的,如果您可以在类初始化时初始化对象,则对象创建也是线程安全的。

以下是一个线程安全的静态初始化对象的示例:

public class MySingletonClass{

   private MySingletonClass(){

   }
   public static MySingletonClass getInstance(){
         return IntiailizationOnDemandClassholder.instance;
   }

   private static class IntiailizationOnDemandClassHolder{
         private static final MySingletonClass instance = new MySingletonClass();

   }

}

在这里需要知道的是,只有在调用 getInstance() 方法时,MySingletonClass 实例变量才会被创建和初始化。并且由于类初始化是线程安全的,因此 InitializationOnDemandClassholder 的 instance 变量将安全地加载一次,并对所有线程可见。

回答你的修改取决于你的其他实现方式。如果您想要进行双重检查锁定,则需要使实例变量成为 volatile。如果您不想使用 DCL,则每次访问变量都需要同步。以下是两个示例:

public class DCLLazySingleton{
  private static volatile DCLLazySingleton instance;

  public static DCLLazySingleton getInstace(){
     if(instance == null){
        synchronized(DCLLazySingleton.class){
            if(instance == null)
                instance=new DCLLazySingleton();
        }
     } 
     return instance;
}
并且
public class ThreadSafeLazySingleton{
   private static ThreadSafeLazySingleton instance;

  public static ThreadSafeLazySingleton getInstance(){
     synchronized(ThreadSafeLazySingleton.class){
        if(instance == null){
            instance = new ThreadSafeLazySingleton();
        }
        return instance;
     } 

}

最后一个示例需要在每次请求实例时进行锁定。第二个示例在每次访问时需要执行volatile-read(可能廉价或不廉价,这取决于CPU)。

第一个示例将始终锁定一次,而不管CPU如何。不仅如此,每次读取都将是普通的,无需担心线程安全。我个人喜欢我列出的第一个示例。


这两种初始化方式有什么区别? - Rachel
@Rachel 双重检查锁定会首先检查对象是否已初始化,如果未初始化,则会锁定类实例以进行创建。然后它会再次检查对象是否已创建,如果没有,则会创建它。由于它持有锁定,因此它将确保仅创建一个实例。DLC的问题在于,如果字段不是易失性的,则无法保证安全的对象初始化。如果您愿意,可以在此处阅读更多内容:http://www.cs.umd.edu/~pugh/java/memoryModel/DoubleCheckedLocking.html - John Vint
@artaxerxe,你能否详细解释一下为什么它与链接相比是有问题的? - John Vint
IntiailizationOnDemandClassHolder示例是线程安全的。虽然您是正确的,new不一定是线程安全的,但类加载是线程安全的。 new赋值发生在释放类加载器锁之前。第二个示例中,new是原子性的,因为该字段是volatile。最后,第三个示例是线程安全的,因为new赋值发生在释放类锁之前和任何随后获取该锁的操作之前。 - John Vint
我进行了一次更新,以更清楚地表明我的最后一个示例是在类锁定下发生的。 - John Vint
显示剩余4条评论

3
我认为在这个演示文稿中,作者指的是静态字段只会在包含该字段的类第一次使用时以线程安全的方式进行初始化 (这由JMM保证)。
class StaticLazyExample1 {

   static Helper helper = new Helper();

   static Helper getHelper() {
      return helper;
   }
}

StaticLazyExample1 类第一次使用时(即构造函数或静态方法调用时),helper 字段会被初始化。

还有一种基于静态延迟初始化的 Initialization On Demand Holder 模式:

class StaticLazyExample2 {

  private static class LazyHolder {
    public static Helper instance = new Helper();
  }

  public static Helper getHelper() {
    return LazyHolder.instance;
  }
}

当首次调用StaticLazyExample2.getHelper()静态方法时,仅创建一个Helper实例。由于静态字段的初始化保证,此代码保证是线程安全和正确的;如果在静态初始化器中设置了字段,则保证对访问该类的任何线程正确可见。

更新

这两种初始化方式有什么区别?

静态延迟初始化提供高效的线程安全延迟初始化static字段,并具有零同步开销。另一方面,如果您希望惰性初始化非静态字段,则应编写以下内容:

class LazyInitExample1 {

  private Helper instance;

  public synchronized Helper getHelper() {
    if (instance == null) instance == new Helper();
    return instance;
  }
}

或者使用双重检查锁定机制:

class LazyInitExample2 {

    private volatile Helper helper;

    public Helper getHelper() {
      if (helper == null) {
          synchronized (this) {
              if (helper == null) helper = new Helper();
          }
      }
      return helper;
    }
}

我需要提醒一下,它们都需要显式同步,并且与静态延迟初始化相比具有额外的时间开销。

这两种初始化方式有什么区别? - Rachel
什么是静态懒加载?我无法理解这个概念。 - Rachel
静态延迟初始化是静态字段的惰性初始化。正如演示所说:“如果您需要初始化单例值,即仅在每个JVM上懒惰地初始化一次的值,请在静态变量的声明中进行初始化”。Java规范保证“它将在类的第一次使用时以线程安全的方式进行初始化,但不会在此之前进行初始化”。请参见我的前两个示例(StaticLazyExample类),了解静态字段的静态延迟初始化示例。 - Idolon
值得一提的是,您不能使用_Static Lazy Initialization_来对非静态字段进行惰性初始化,只能用于静态字段。 - Idolon
好的,那么在你上面的两个例子中,第一个例子不是线程安全的,对吧? - Rachel
如果你在谈论 LazyInitExample 的第一个实例,它是线程安全的,因为 instance 字段是在同步块中写入和读取的。也许我最好从 getInstance 方法中删除 static 修饰符,以免让你感到困惑。 - Idolon

2

值得注意的是,最简单的线程安全静态懒加载初始化方法是使用enum。这是因为静态字段的初始化是线程安全的,并且类本身也是惰性加载的。

enum ThreadSafeLazyLoadedSingleton {
    INSTANCE;
}

使用惰性加载值的类是字符串。hashCode 仅在第一次使用时计算。之后,使用缓存的 hashCode。

我认为不能说其中一个比另一个更好,因为它们实际上是不可互换的。


我正在努力理解你的回答,你能详细说明一下这两种方法之间的区别吗?我特别想了解延迟初始化和静态延迟初始化之间的实际区别,同时我也不确定演示者在解释静态延迟初始化时想要表达什么意思。 - Rachel
据我所知,静态懒加载是静态字段的懒加载。更广泛的懒加载术语是任何懒加载。(这可能包括静态的)我倾向于使用非静态懒加载来表示非静态字段。你之所以要区分它们,是因为它们有不同的用途。例如,单例始终是静态的。 - Peter Lawrey

1

在这里提供一个参考肯定是好的。它们都有相同的基本思想:如果不必要,为什么要分配资源(内存、CPU)?相反,推迟分配这些资源,直到它们实际需要。这可以在高强度环境中避免浪费,但如果您需要立即获得结果并且不能等待,则可能非常糟糕。添加一个“懒惰但谨慎”的系统非常困难(检测停机时间并在空闲时间运行这些懒惰计算的系统)。

以下是懒惰初始化的示例。

class Lazy {

    String value;
    int computed;

    Lazy(String s) { this.value = s; }

    int compute() {
        if(computed == 0) computed = value.length();
        return computed;
    }

}

这里是静态延迟初始化

class StaticLazy {

    private StaticLazy staticLazy;
    static StaticLazy getInstance() {
        if(staticLazy == null) staticLazy = new StaticLazy();
        return staticLazy;
    }
}

你能解释一下它们的区别吗? - Rachel
如果我正确理解了您的评论,那么为什么应该选择静态延迟初始化,似乎总是应该选择延迟初始化。 - Rachel
Youtube在我们公司被屏蔽了,所以很遗憾我无法观看你的视频来了解讲师的原因。就我个人而言,我并没有看出它们之间有任何区别。可能视频中的讲师和我有不同的观点,或者更有可能是定义上的差异。我会尝试记得稍后再观看这些视频,也许我可以回来跟你交流。 - corsiKa
当然,是从第43分钟到第48分钟,这5分钟试图解释静态延迟初始化,我也无法理解两种延迟初始化之间的区别,也许您可以更准确地解释一下它们的区别? - Rachel

0

区别在于您实现延迟初始化的机制。通过“静态延迟初始化”,我认为演示者指的是this solution,它依赖于JVM遵守任何版本的Java(请参见Java语言规范的12.4类和接口的初始化)。

“懒惰初始化”可能意味着许多其他答案中描述的懒惰初始化。这种初始化机制对JVM做出了不安全的线程假设,直到Java 5才具有真正的内存模型规范。


0
  1. 懒加载只是一个花哨的名字,用于在实际需要时初始化类。

  2. 简单来说,懒加载是一种软件设计模式,其中对象的初始化仅在实际需要时发生,而不是在之前进行,以保持使用的简单性并提高性能。

  3. 当对象创建的成本非常高且对象的使用非常罕见时,懒加载是必不可少的。因此,在这种情况下,值得实现懒加载。懒加载的基本思想是在需要时加载对象/数据。

来源: https://www.geeksforgeeks.org/lazy-loading-design-pattern/


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接