返回

双重检查锁定:揭示Java并发中的陷阱

见解分享

引言

在构建健壮、高性能的多线程应用程序时,双重检查锁定(DCL)是一种备受推崇的模式。虽然DCL被设计用于优化并发的延迟初始化,但它在Java中却潜藏着微妙的陷阱。本文深入探讨了双重检查锁定模式,揭示了它在Java中的局限性,并提出了最佳实践以避免这些陷阱。

双重检查锁定的原理

双重检查锁定是一种设计模式,旨在提高多线程环境下延迟初始化的效率。它利用了Java虚拟机(JVM)的内存模型,该模型保证了对volatile变量的写操作将立即对所有线程可见。DCL模式的实现如下:

private volatile Object instance;

public Object getInstance() {
  if (instance == null) {
    synchronized (this) {
      if (instance == null) {
        instance = new Object();
      }
    }
  }
  return instance;
}

在这个实现中,instance变量被标记为volatile,这确保了对它的写入操作会立即对所有线程可见。当一个线程首次调用getInstance()方法时,它会检查instance是否为null。如果为null,它将通过同步块对其进行加锁。在同步块内,它再次检查instance是否为null。如果仍然为null,它将创建一个新实例并将其存储在instance中。这种双重检查减少了对同步块的调用次数,从而提高了多线程环境下的性能。

Java中的陷阱

尽管DCL模式在单线程环境下有效,但在Java多线程环境中却存在一些陷阱。这些陷阱源于Java内存模型的微妙性。

指令重排序

Java内存模型允许JVM对代码指令进行重新排序以提高性能。这可能会破坏双重检查锁定模式的预期行为。例如,在上面的实现中,JVM可能会重新排序以下指令:

instance = null;
instance = new Object();

这意味着可能存在一种情况,线程A读取到instance为null,然后立即被中断。在此期间,线程B获得了锁,创建了实例并将其存储在instance中。当线程A恢复执行时,它将看到instance不再为null,并使用未初始化的实例。这会导致不可预测的行为。

无序写

另一个陷阱是,Java内存模型并不要求将对volatile变量的写入按程序顺序执行。这可能会导致线程A读取instance为null,然后被中断。在此期间,线程B创建了一个实例并将其存储在instance中,但由于无序写,线程A可能仍然看不到该写入。当线程A恢复执行时,它将看到instance仍然为null,并会重复初始化过程,这可能会导致创建多个实例。

解决办法

为了避免双重检查锁定模式在Java中的陷阱,有几种最佳实践:

  • 使用synchronized :最安全的方法是使用synchronized关键字显式同步对instance变量的访问。这样可以确保所有线程都将按照预期顺序执行初始化过程。
private Object instance;

public Object getInstance() {
  synchronized (this) {
    if (instance == null) {
      instance = new Object();
    }
  }
  return instance;
}
  • 使用java.util.concurrent.ConcurrentHashMap :ConcurrentHashMap是一个线程安全的集合,可以在不使用同步的情况下高效地存储和检索对象。
private ConcurrentHashMap<String, Object> instanceMap = new ConcurrentHashMap<>();

public Object getInstance(String key) {
  return instanceMap.computeIfAbsent(key, k -> new Object());
}
  • 使用枚举 :如果实例的类型是有限的且已知的,可以使用枚举来实现单例模式。枚举是线程安全的,因此不需要任何额外的同步机制。
public enum Singleton {
  INSTANCE;

  public Object getInstance() {
    return this;
  }
}

结论

双重检查锁定模式是一种优化多线程延迟初始化的有效模式。然而,在Java中使用时,它潜藏着一些陷阱。通过遵循本文提出的最佳实践,开发人员可以避免这些陷阱并编写出健壮、高性能的多线程应用程序。