java单例——Java 内存模型之从 JMM 角度分析 DCL

简介: java单例——Java 内存模型之从 JMM 角度分析 DCL

DCL ,即 Double Check Lock ,中文称为“双重检查锁定”。

其实 DCL 很多人在单例模式中用过,LZ 面试人的时候也要他们写过,但是有很多人都会写错。他们为什么会写错呢?其错误根源在哪里?有什么解决方案?下面就随 LZ 一起来分析。

1. 问题分析

我们先看单例模式里面的懒汉式:

public class Singleton {
 private static Singleton singleton;
 private Singleton(){}
 public static Singleton getInstance(){
 if (singleton == null) {
 singleton = new Singleton();
 }
 return singleton;
 }
}

我们都知道这种写法是错误的,因为它无法保证线程的安全性。优化如下:


public class Singleton {
 private static Singleton singleton;
 private Singleton(){}
 public static synchronized Singleton getInstance() {
 if (singleton == null) {
 singleton = new Singleton();
 }
 return singleton;
 }
}

优化非常简单,就是在 #getInstance() 方法上面做了同步,但是 synchronized 就会导致这个方法比较低效,导致程序性能下降,那么怎么解决呢?聪明的人们想到了双重检查 DCL:

public class Singleton {
 private static Singleton singleton;
 private Singleton() {}
 public static Singleton getInstance(){
 if(singleton == null){ // 1
 synchronized (Singleton.class){ // 2
 if(singleton == null){ // 3
 singleton = new Singleton(); // 4
 }
 }
 }
 return singleton;
 }
}

就如上面所示,这个代码看起来很完美,理由如下:

  • 如果检查第一个 singleton 不为 null ,则不需要执行下面的加锁动作,极大提高了程序的性能。
  • 如果第一个 singleton 为 null ,即使有多个线程同一时间判断,但是由于 synchronized 的存在,只会有一个线程能够创建对象。
  • 当第一个获取锁的线程创建完成后 singleton 对象后,其他的在第二次判断 singleton 一定不会为 null ,则直接返回已经创建好的 singleton 对象。

通过上面的分析,DCL 看起确实是非常完美,但是可以明确地告诉你,这个错误的。上面的逻辑确实是没有问题,分析也对,但是就是有问题,那么问题出在哪里呢?在回答这个问题之前,我们先来复习一下创建对象过程,实例化一个对象要分为三个步骤:

memory = allocate();   //1:分配内存空间
ctorInstance(memory);  //2:初始化对象
instance = memory;     //3:将内存空间的地址赋值给对应的引用

但是由于重排序的原因,步骤 2、3 可能会发生重排序,其过程如下:

memory = allocate();   // 1:分配内存空间
instance = memory;     // 3:将内存空间的地址赋值给对应的引用
                                    // ? 注意,此时对象还没有被初始化!
ctorInstance(memory); // 2:初始化对象

如果 2、3 发生了重排序,就会导致第二个判断会出错,singleton != null,但是它其实仅仅只是一个地址而已,此时对象还没有被初始化,所以 returnsingleton 对象是一个没有被初始化的对象,如下:


按照上面图例所示,线程 B 访问的是一个没有被初始化的 singleton 对象。

通过上面的阐述,我们可以判断 DCL 的错误根源在于步骤 4:

singleton = new Singleton();

知道问题根源所在,那么怎么解决呢?有两个解决办法:

  1. 不允许初始化阶段步骤 2、3 发生重排序。
  2. 允许初始化阶段步骤 2、3 发生重排序,但是不允许其他线程“看到”这个重排序。

2. 解决方案

解决方案依据上面两个解决办法即可。

2.1 基于 volatile 解决方案

对于上面的DCL其实只需要做一点点修改即可:将变量singleton生命为volatile即可:

public class Singleton {
 // 通过volatile关键字来确保安全
 private volatile static Singleton singleton;
 private Singleton(){}
 public static Singleton getInstance(){
 if(singleton == null){
 synchronized (Singleton.class){
 if(singleton == null){
 singleton = new Singleton();
 }
 }
 }
 return singleton;
 }
}

singleton 声明为 volatile后,步骤 2、3 就不会被重排序了,也就可以解决上面那问题了。

2.2 基于类初始化的解决方案

该解决方案的根本就在于:利用 ClassLoder 的机制,保证初始化 instance 时只有一个线程。JVM 在类初始化阶段会获取一个锁,这个锁可以同步多个线程对同一个类的初始化。

public class Singleton {
 private static class SingletonHolder{
 public static Singleton singleton = new Singleton();
 }
 public static Singleton getInstance(){
 return SingletonHolder.singleton;
 }
}

Java 语言规定,对于每一个类或者接口 C ,都有一个唯一的初始化锁 LC 与之相对应。从C 到 LC 的映射,由 JVM 的具体实现去自由实现。JVM 在类初始化阶段期间会获取这个初始化锁,并且每一个线程至少获取一次锁来确保这个类已经被初始化过了。

老艿艿:因为基于类初始化的解决方案,涉及到类加载机制,本文就不拓展开来,感兴趣的胖友,可以看看 《双重检查锁定与延迟初始化》「基于类初始化的解决方案」 小节。

3. 总结

延迟初始化降低了初始化类或创建实例的开销,但增加了访问被延迟初始化的字段的开销。在大多数时候,正常的初始化要优于延迟初始化。

  • 如果确实需要对实例字段使用线程安全的延迟初始化,请使用上面介绍的基于 volatile 的延迟初始化的方案。
  • 如果确实需要对静态字段使用线程安全的延迟初始化,请使用上面介绍的基于类初始化的方案。

参考资料

  1. 方腾飞:《Java并发编程的艺术》
  2. 程晓明:《双重检查锁定与延迟初始化》


相关文章
|
1月前
|
Web App开发 监控 JavaScript
监控和分析 JavaScript 内存使用情况
【10月更文挑战第30天】通过使用上述的浏览器开发者工具、性能分析工具和内存泄漏检测工具,可以有效地监控和分析JavaScript内存使用情况,及时发现和解决内存泄漏、过度内存消耗等问题,从而提高JavaScript应用程序的性能和稳定性。在实际开发中,可以根据具体的需求和场景选择合适的工具和方法来进行内存监控和分析。
|
29天前
|
存储 缓存 安全
Java内存模型深度解析:从理论到实践####
【10月更文挑战第21天】 本文深入探讨了Java内存模型(JMM)的核心概念与底层机制,通过剖析其设计原理、内存可见性问题及其解决方案,结合具体代码示例,帮助读者构建对JMM的全面理解。不同于传统的摘要概述,我们将直接以故事化手法引入,让读者在轻松的情境中领略JMM的精髓。 ####
37 6
|
20天前
|
安全 Java 程序员
深入理解Java内存模型与并发编程####
本文旨在探讨Java内存模型(JMM)的复杂性及其对并发编程的影响,不同于传统的摘要形式,本文将以一个实际案例为引子,逐步揭示JMM的核心概念,包括原子性、可见性、有序性,以及这些特性在多线程环境下的具体表现。通过对比分析不同并发工具类的应用,如synchronized、volatile关键字、Lock接口及其实现等,本文将展示如何在实践中有效利用JMM来设计高效且安全的并发程序。最后,还将简要介绍Java 8及更高版本中引入的新特性,如StampedLock,以及它们如何进一步优化多线程编程模型。 ####
21 0
|
22天前
|
安全 Java 程序员
Java内存模型的深入理解与实践
本文旨在深入探讨Java内存模型(JMM)的核心概念,包括原子性、可见性和有序性,并通过实例代码分析这些特性在实际编程中的应用。我们将从理论到实践,逐步揭示JMM在多线程编程中的重要性和复杂性,帮助读者构建更加健壮的并发程序。
|
25天前
|
并行计算 算法 测试技术
C语言因高效灵活被广泛应用于软件开发。本文探讨了优化C语言程序性能的策略,涵盖算法优化、代码结构优化、内存管理优化、编译器优化、数据结构优化、并行计算优化及性能测试与分析七个方面
C语言因高效灵活被广泛应用于软件开发。本文探讨了优化C语言程序性能的策略,涵盖算法优化、代码结构优化、内存管理优化、编译器优化、数据结构优化、并行计算优化及性能测试与分析七个方面,旨在通过综合策略提升程序性能,满足实际需求。
56 1
|
27天前
|
JavaScript
如何使用内存快照分析工具来分析Node.js应用的内存问题?
需要注意的是,不同的内存快照分析工具可能具有不同的功能和操作方式,在使用时需要根据具体工具的说明和特点进行灵活运用。
41 3
|
1月前
|
Java
Java内存模型
JMM(Java内存模型 )屏蔽了各种硬件和操作系统的内存访问差异,实现让Java程序在各平台下都能达到一致的内存访问效果,它定义了JVM如何将程序中的变量在主存中读取 具体定义为:所有变量都存在主存中,主存是线程共享区域;每个线程都有自己独有的工作内存,线程想要操作变量必须从主从中copy变量到自己的工作区,每个线程的工作内存是相互隔离的 由于主存与工作内存之间有读写延迟,且读写不是原子性操作,所以会有线程安全问题
|
1月前
|
缓存 算法 Java
本文聚焦于Java内存管理与调优,介绍Java内存模型、内存泄漏检测与预防、高效字符串拼接、数据结构优化及垃圾回收机制
在现代软件开发中,性能优化至关重要。本文聚焦于Java内存管理与调优,介绍Java内存模型、内存泄漏检测与预防、高效字符串拼接、数据结构优化及垃圾回收机制。通过调整垃圾回收器参数、优化堆大小与布局、使用对象池和缓存技术,开发者可显著提升应用性能和稳定性。
51 6
|
1月前
|
开发框架 监控 .NET
【Azure App Service】部署在App Service上的.NET应用内存消耗不能超过2GB的情况分析
x64 dotnet runtime is not installed on the app service by default. Since we had the app service running in x64, it was proxying the request to a 32 bit dotnet process which was throwing an OutOfMemoryException with requests >100MB. It worked on the IaaS servers because we had the x64 runtime install
|
1月前
|
存储 缓存 安全
Java内存模型(JMM):深入理解并发编程的基石####
【10月更文挑战第29天】 本文作为一篇技术性文章,旨在深入探讨Java内存模型(JMM)的核心概念、工作原理及其在并发编程中的应用。我们将从JMM的基本定义出发,逐步剖析其如何通过happens-before原则、volatile关键字、synchronized关键字等机制,解决多线程环境下的数据可见性、原子性和有序性问题。不同于常规摘要的简述方式,本摘要将直接概述文章的核心内容,为读者提供一个清晰的学习路径。 ####
47 2