如何优雅地实现单例模式?内部静态类还是双重检查锁定?

在最近的一个项目中,我需要为一个核心配置类实现单例模式。在设计过程中,我发现要同时满足延迟加载线程安全这两个要求,常见的实现方式有两种:内部静态类双重检查锁定(Double-Checked Locking, DCL)。

起初,我倾向于使用 DCL,它通过双重检查来避免不必要的同步开销,但需要谨慎处理 volatile 关键字的使用,确保在多线程环境下的安全性。另一方面,内部静态类的实现更加简洁,利用类加载的机制,天然地保证了线程安全和延迟加载。但这两者在实际应用中各有优劣,那么在面对不同场景时,究竟该如何选择更合适的单例实现方式呢?

内部静态类(Bill Pugh Singleton Pattern)

内部静态类是一种 基于类加载机制 的懒加载实现方式。静态内部类中的实例只在第一次使用时初始化,JVM 在类加载时会保证这个过程是线程安全的。

静态内部类不会随着外部类的加载和初始化而初始化,它只会在被调用时才加载。这利用了 Java 类加载机制的延迟加载特性,同时由 JVM 确保了类的加载过程是线程安全的。

示例代码

public class Singleton {private Singleton() {}// 静态内部类,负责持有 Singleton 实例private static class SingletonHolder {private static final Singleton INSTANCE = new Singleton();}// 获取 Singleton 实例public static Singleton getInstance() {return SingletonHolder.INSTANCE;}
}
优点:
  1. 线程安全:JVM 类加载机制保证了静态内部类的初始化是线程安全的,避免了显式的同步控制。
  2. 懒加载:内部静态类只有在首次调用时才会加载,实现了延迟初始化。
  3. 高效:没有锁和同步块的开销,性能较好。
  4. 代码简单清晰:相比 DCL,代码结构更简洁,不易出错。
缺点:
  • 适用性有限:静态内部类的方式只能用于单例模式,并且依赖于类加载机制,如果需要实现其他类型的延迟加载或更加复杂的对象初始化流程,可能不适用。
使用场景:
  • 适用于创建单例对象时对性能要求较高的场景,同时需要保证线程安全性。例如,在某些性能敏感的库或框架中可以使用这种方式来延迟加载资源。

双重检查锁定(Double-Checked Locking, DCL)

双重检查锁定是一种通过手动控制线程同步来实现延迟加载的模式。其核心思想是:在多线程访问单例时,第一次检查实例是否为 null,如果是 null,则进入同步代码块,再次检查实例是否为 null,如果依然为 null,才创建实例。这种方式可以减少不必要的同步,提升性能。

DCL 依赖于 volatile 关键字来保证线程间的可见性。volatile 确保变量的写操作对所有线程可见,防止指令重排序带来的问题(例如对象未完全构造好就被引用)。

示例代码

public class Singleton {private static volatile Singleton instance;private Singleton() {}public static Singleton getInstance() {if (instance == null) { // 第一次检查synchronized (Singleton.class) {if (instance == null) { // 第二次检查instance = new Singleton();}}}return instance;}
}
优点:
  1. 延迟加载:和静态内部类一样,DCL 也是一种延迟加载的单例实现方式。
  2. 控制更灵活:可以用于更复杂的初始化过程,例如在创建对象时需要加载资源或执行初始化操作。
  3. 节省资源:通过第一次非同步检查避免了每次获取实例时的同步开销,从而提高性能。
缺点:
  1. 实现复杂性:代码复杂,容易出现错误,例如忘记使用 volatile 会导致线程间的可见性问题。
  2. 性能损耗:虽然比直接使用 synchronized 好,但在多核处理器上,由于 volatile 的开销,性能可能还是会受到影响。
  3. 依赖于 Java 版本:在 Java 5 之前,volatile 没有确保指令重排序的保障,可能导致双重检查锁定失效。但自 Java 5 起,JVM 对 volatile 的支持增强了,DCL 可以安全使用。
使用场景:

双重检查锁定(DCL)适用于那些需要在多线程环境下延迟加载复杂资源的场景,并且对性能有要求的场景。这里的复杂场景指的是:对象的创建不仅仅是简单的实例化,而是需要依赖外部资源的加载、进行多步初始化,甚至是需要根据特定条件执行不同的初始化流程。在这种情况下,双重检查锁定可以保证只有在需要时才创建资源,同时确保初始化过程是线程安全的,并避免每次获取实例时的性能损耗。

示例场景:数据库连接池的延迟初始化

在某些大型系统中,数据库连接池的初始化可能非常复杂。假设一个应用程序只有在某些条件满足时才需要与数据库交互,为了节省资源,不希望在程序启动时就立即初始化数据库连接池,而是希望在第一次需要数据库时才初始化。

另外,数据库连接池的创建过程可能包括以下步骤:

  1. 加载配置文件。
  2. 从数据库驱动程序工厂获取连接。
  3. 设置各种连接池参数,如最大连接数、超时时间等。
  4. 启动连接池监控线程。
  5. 其他初始化工作。

由于创建数据库连接池涉及多个步骤,且初始化过程需要确保只有一个线程能成功创建实例,因此可以使用双重检查锁定来保证线程安全。

示例代码

public class DatabaseConnectionPool {// 用于保存连接池的单例实例private static volatile DatabaseConnectionPool instance;// 私有的构造方法,防止外部实例化private DatabaseConnectionPool() {// 模拟连接池的复杂初始化过程initializeConnectionPool();}// 获取连接池实例的静态方法public static DatabaseConnectionPool getInstance() {if (instance == null) {  // 第一次检查synchronized (DatabaseConnectionPool.class) {if (instance == null) {  // 第二次检查instance = new DatabaseConnectionPool();}}}return instance;}// 初始化连接池的方法,假设涉及多个复杂步骤private void initializeConnectionPool() {// 1. 加载数据库配置loadConfiguration();// 2. 从数据库驱动程序工厂获取连接initializeConnections();// 3. 设置连接池参数configurePoolParameters();// 4. 启动连接池监控线程startConnectionMonitor();// 其他初始化步骤...}private void loadConfiguration() {// 加载数据库连接的配置信息System.out.println("加载数据库配置...");}private void initializeConnections() {// 初始化数据库连接System.out.println("初始化数据库连接...");}private void configurePoolParameters() {// 设置连接池参数,例如最大连接数、超时设置等System.out.println("配置连接池参数...");}private void startConnectionMonitor() {// 启动一个后台线程来监控连接池的健康状态System.out.println("启动连接池监控线程...");}// 模拟获取数据库连接的方法public void getConnection() {System.out.println("获取数据库连接...");}
}

说明:

  1. 延迟加载DatabaseConnectionPool 的实例只有在调用 getInstance() 时才会初始化。这样,当程序启动时,如果没有需要数据库操作,就不会浪费资源去初始化连接池。

  2. 复杂的初始化过程initializeConnectionPool() 方法模拟了连接池初始化的多个步骤,例如加载配置文件、设置参数、启动监控线程等。这些步骤需要确保线程安全,因为在多线程环境下,可能有多个线程同时试图获取连接池的实例。

  3. 双重检查锁定

    • 第一次检查:if (instance == null)。如果已经有实例了,就直接返回,避免进入同步块,从而减少不必要的同步开销。
    • 第二次检查:synchronized 块内的 if (instance == null)。这是为了防止多线程同时通过第一次检查,确保只有一个线程能创建实例,其他线程将等待第一个线程完成实例化。

场景适用性分析

  1. 资源开销大:数据库连接池的创建涉及外部资源的调用、参数的配置,尤其是在高并发场景下,数据库连接是有限的。每次初始化连接池都需要消耗较多时间和资源,所以使用延迟加载能有效避免不必要的开销。

  2. 线程安全要求高:由于数据库连接池是共享的资源,所有线程都会使用同一个连接池实例。如果不保证初始化过程的线程安全,可能会导致多个线程创建多个连接池实例,浪费资源甚至引发冲突。

  3. 多线程访问:例如,在一个 Web 应用中,多个用户请求可能同时访问数据库。如果在请求高峰期第一次访问数据库时并发创建连接池,没有双重检查锁定,可能会导致多个线程同时初始化连接池,造成不必要的性能损耗。

其他复杂初始化场景

除了数据库连接池,还有其他一些场景可能适合使用 DCL:

  • 缓存系统的延迟初始化:有时应用程序需要在运行时动态加载缓存数据,初始缓存数据可能需要从外部服务或文件中加载。如果在多个线程同时访问时没有同步机制,可能会导致缓存系统加载重复的资源。

  • 配置管理器的延迟加载:在分布式系统中,配置管理器可能需要从多个外部资源加载配置文件(如读取远程配置中心、合并本地和远程配置),这种初始化过程也是多步骤的,且线程安全要求很高。

  • 日志系统的初始化:日志系统的初始化通常涉及创建文件句柄、建立远程连接、设置格式化器等多步骤操作。在高并发环境下,多个线程同时访问日志系统时,也需要确保日志系统的初始化是安全且高效的。


内部静态类 vs 双重检查锁定的对比

特性静态内部类双重检查锁定(DCL)
实现复杂性简单,代码清晰复杂,容易出错
线程安全JVM 保证线程安全需要手动保证(volatile
延迟加载
性能开销无锁开销,性能高volatile 和锁开销
适用场景适合单例模式适合更复杂的初始化场景
JVM 依赖无需依赖 volatile依赖 volatile 和 Java 版本
扩展性一般,适合单例模式灵活,适合复杂的延迟初始化

场景选择

  • 静态内部类:适合在需要高效、简洁且线程安全的场景下实现单例模式,且不需要复杂的初始化流程。这种方式通常是推荐的单例实现方式,特别是性能要求较高的场合。
  • 双重检查锁定:适合需要复杂初始化逻辑的场景,或者在一些特殊的情况下,可能会涉及到需要动态控制单例对象的创建流程。尽管代码复杂性较高,但在 Java 5 之后的版本中,已经可以安全地使用双重检查锁定。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/web/62247.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【计算机网络】 —— 数据链路层(壹)

文章目录 前言 一、概述 1. 基本概念 2. 数据链路层的三个主要问题 二、封装成帧 1. 概念 2. 帧头、帧尾的作用 3. 透明传输 4. 提高效率 三、差错检测 1. 概念 2. 奇偶校验 3. 循环冗余校验CRC 1. 步骤 2. 生成多项式 3. 例题 4. 总结 四、可靠传输 1. 基本…

golang实现简单的redis服务

golang 手搓redis服务器仓库地址:实现思路: golang 手搓redis服务器 仓库地址: 仓库: https://github.com/dengjiayue/my-redis.git 实现思路: ● 协议: tcp通信 ● 数据包: 长度(4byte)方法(1byte)数据json ● 数据处理: 单线程map读写 ○ 依次处理待处理队列的请求(chan)…

智慧银行反欺诈大数据管控平台方案(八)

智慧银行反欺诈大数据管控平台的核心理念,在于通过整合先进的大数据技术、算法模型和人工智能技术,构建一个全面、智能、动态的反欺诈管理框架,以实现对金融交易的全方位监控、欺诈行为的精准识别和高效处理。这一理念强调数据驱动决策&#…

3D 生成重建019-LERF用文本在Nerf中开启上帝之眼

3D 生成重建019-LERF用文本在Nerf中开启上帝之眼 文章目录 0 论文工作1 论文方法2 实验结果 0 论文工作 人类利用自然语言描述物理世界,根据各种特性(视觉外观、语义、抽象关联)寻找具体的3D位置。在这项工作中,作者提出了语言嵌…

如何选择合适的期刊投稿?从课题组经验到在线工具的使用全解析

~~~本文是作者个人的经验分享,建立在导师让自己选刊的情况下~~~ 投稿选刊是科研过程中至关重要的一步,选刊过程可能让许多初投稿的研究者感到迷茫和困惑:期刊那么多,如何找到最合适的? 本文将从多个角度介绍如何选择投…

024、Docker与SSH在分布式系统中的实践指南

1. Docker SSH配置最佳实践 Docker容器通常不需要SSH服务来运行,因为它们设计为轻量级、无状态的,并且通常通过Docker命令行界面与宿主机进行交互。但是,在某些情况下,您可能需要通过SSH访问Docker容器进行调试、维护或其他操作。…

【kafka】消息队列的认识,Kafka与RabbitMQ的简单对比

什么是消息队列? 消息队列(Message Queue,简称 MQ)是一个在不同应用程序、系统或服务之间传递数据的机制。 它允许系统间异步地交换信息,而无需直接交互,确保消息的可靠传输。 想象一下,你正在…

.NET MAUI与.NET for Android/IOS的关系

2024年11月13日微软发布了.Net9.0,我打算体验一下。安装好.Net9.0 SDK后发现Visual Studio识别不到9.0,但是通过命令行dotnet --info查看是正常的,后面看到了VS有版本可以升级,把VS升级到17.12.0就可以了。更新完打开以后看到如下界面 这里…

SqlDataAdapter

SqlDataAdapter 是 .NET Framework 和 .NET Core 中提供的一个数据适配器类,属于 System.Data.SqlClient 命名空间(或在 .NET 6 中属于 Microsoft.Data.SqlClient 命名空间)。它的作用是充当数据源(如 SQL Server 数据库&#xff…

【vivado】时序报告--best时序和worst时序

利用vivado进行开发时,生成best时序报告和worst时序报告。 best时序报告 slow选择min_max,fast选择none。 worst时序报告 fast选择min_max,slow选择none。

FastAPI 响应状态码:管理和自定义 HTTP Status Code

FastAPI 响应状态码:管理和自定义 HTTP Status Code 本文介绍了如何在 FastAPI 中声明、使用和修改 HTTP 状态码,涵盖了常见的 HTTP 状态码分类,如信息响应(1xx)、成功状态(2xx)、客户端错误&a…

力扣题库-掷骰子模拟详细解析

题目如下: 有一个骰子模拟器会每次投掷的时候生成一个 1 到 6 的随机数。 不过我们在使用它时有个约束,就是使得投掷骰子时,连续 掷出数字 i 的次数不能超过 rollMax[i](i 从 1 开始编号)。 现在,给你一…

深入浅出:PHP中的数据类型全解析

文章目录 引言理解数据类型标量类型整数 (integer)浮点数 (float)布尔值 (boolean)字符串 (string) 复合类型数组 (array)对象 (object)资源 (resource)NULL 特殊类型Callable强制类型转换 实战案例总结与展望参考资料 引言 在编程的世界里,数据类型是构建任何应用…

当linux可执行文件缺少或者不兼容so库时候,如何查看版本以及缺少那些库

解决方法: ldd 命令来验证程序是否加载了正确的库: 如检查linear_elasticity可执行文件缺少的库,用下面命令: ldd linear_elasticity 可以发现下面not found就是缺少的库,还有对应的库的位置已经版本 $ ldd lin…

第P1周:Pytorch实现mnist手写数字识别

🍨 本文为🔗365天深度学习训练营 中的学习记录博客🍖 原作者:K同学啊 目标 1. 实现pytorch环境配置 2. 实现mnist手写数字识别 3. 自己写几个数字识别试试具体实现 (一)环境 语言环境:Python…

Seq2Seq模型的发展历史;深层RNN结构为什么出现梯度消失/爆炸问题,Transformer为什么不会;Seq2Seq模型存在问题

目录 Seq2Seq模型的发展历史 改进不足的地方 深层RNN结构为什么出现梯度消失/爆炸问题,Transformer为什么不会 深层RNN结构为什么出现梯度消失/爆炸问题: Transformer为什么不会出现梯度消失/爆炸问题: Seq2Seq模型存在问题 T5模型介绍 Seq2Seq模型的发展历史 序列到…

网络安全技术详解:虚拟专用网络(VPN) 安全信息与事件管理(SIEM)

虚拟专用网络(VPN)详细介绍 虚拟专用网络(VPN)通过在公共网络上创建加密连接来保护数据传输的安全性和隐私性。 工作原理 VPN的工作原理涉及建立安全隧道和数据加密: 隧道协议:使用协议如PPTP、L2TP/IP…

Hive 窗口函数与分析函数深度解析:开启大数据分析的新维度

Hive 窗口函数与分析函数深度解析:开启大数据分析的新维度 在当今大数据蓬勃发展的时代,Hive 作为一款强大的数据仓库工具,其窗口函数和分析函数犹如一把把精巧的手术刀,助力数据分析师们精准地剖析海量数据,挖掘出深…

SCAU期末笔记 - 数据库系统概念

我校使用Database System Concepts,9-12章不考所以跳过,因为课都逃了所以复习很仓促,只准备过一下每一章最后的概念辨析,我也不知道有没有用 第1章 引言 数据库管理系统(DBMS) 由一个互相关联的数据的集合…

Android 12系统源码_窗口管理(九)深浅主题切换流程源码分析

前言 上一篇我们简单介绍了应用的窗口属性WindowConfiguration这个类,该类存储了当前窗口的显示区域、屏幕的旋转方向、窗口模式等参数,当设备屏幕发生旋转的时候就是通过该类将具体的旋转数据传递给应用的、而应用在加载资源文件的时候也会结合该类的A…