Deprecated: imwpcache\f884414bce24ee67f\f73723ec7b1919fa5::__construct(): Implicitly marking parameter $YECBGYFECGEAFWHA as nullable is deprecated, the explicit nullable type must be used instead in /www/wwwroot/www.chuangxiangniao.com/wp-content/plugins/imwpcache-dist/build/f884414bce24ee67ff73723ec7b1919fa5.php on line 2

Deprecated: imwpcache\f884414bce24ee67f\f73723ec7b1919fa5::__construct(): Implicitly marking parameter $BBWFDDBHHYHDXXAB as nullable is deprecated, the explicit nullable type must be used instead in /www/wwwroot/www.chuangxiangniao.com/wp-content/plugins/imwpcache-dist/build/f884414bce24ee67ff73723ec7b1919fa5.php on line 2
如何实现一个线程安全的单例?_创想鸟

如何实现一个线程安全的单例?

答案:双重检查锁定(DCL)通过volatile关键字和同步块确保线程安全,防止指令重排序与内存可见性问题,实现高效懒加载单例。

如何实现一个线程安全的单例?

实现一个线程安全的单例模式,核心在于确保在多线程并发访问时,类的实例只会被创建一次。这通常通过延迟初始化(Lazy Initialization)结合恰当的同步机制来达成,其中“双重检查锁定”(Double-Checked Locking, DCL)是一个非常经典且高效的策略,尤其是在Java这类语言中,配合

volatile

关键字使用,能有效解决并发问题并保证性能。

解决方案

在Java中,实现线程安全的单例,我个人比较倾向于使用双重检查锁定(DCL)模式,因为它在保证线程安全的同时,兼顾了性能,避免了不必要的同步开销。

public class ThreadSafeSingleton {    // 使用 volatile 关键字确保多线程环境下,对 instance 的修改能立即被其他线程看到    // 并且防止指令重排序,这是 DCL 模式的关键所在。    private static volatile ThreadSafeSingleton instance;    // 私有构造器,阻止外部直接创建实例    private ThreadSafeSingleton() {        // 防止通过反射机制创建多个实例,可以抛出异常        if (instance != null) {            throw new RuntimeException("请使用 getInstance() 方法获取单例实例。");        }        // 这里可以有一些初始化逻辑        System.out.println("单例实例正在被创建...");    }    // 公有静态方法,提供全局访问点    public static ThreadSafeSingleton getInstance() {        // 第一次检查:如果实例已经存在,直接返回,避免进入同步块,提高性能        if (instance == null) {            // 同步块:确保只有一个线程能进入创建实例            synchronized (ThreadSafeSingleton.class) {                // 第二次检查:在同步块内部再次检查,防止多线程下重复创建                if (instance == null) {                    instance = new ThreadSafeSingleton();                }            }        }        return instance;    }    public void showMessage() {        System.out.println("Hello from the Singleton!");    }}

这段代码的核心思想是:先进行一次非同步的

null

检查。如果实例已经存在,就直接返回,这样后续的线程就不会进入同步块,大大减少了锁的竞争。只有当实例为

null

时,才进入同步块。进入同步块后,会进行第二次

null

检查,这是为了防止在第一个线程创建实例的过程中,第二个线程也通过了第一次

null

检查并等待进入同步块。当第一个线程释放锁后,第二个线程进入同步块,如果不再检查一次,它就会再次创建一个实例,从而破坏单例。

volatile

关键字在这里的作用至关重要,它确保了

instance

变量的可见性以及禁止了指令重排序,这我们后面会详细聊聊。

为什么普通的单例模式在多线程环境下会失效?

这个问题其实挺有意思的,它揭示了并发编程中一个非常基础但又容易被忽视的“坑”。想象一下,如果我们的单例模式是那种最简单的懒汉式,也就是

getInstance()

方法没有加任何同步措施:

public class SimpleSingleton {    private static SimpleSingleton instance;    private SimpleSingleton() {}    public static SimpleSingleton getInstance() {        if (instance == null) { // 检查实例是否为null            instance = new SimpleSingleton(); // 如果是null,就创建        }        return instance;    }}

在单线程环境下,这当然没问题。但一旦我们引入了多线程,麻烦就来了。假设有两个线程(Thread A 和 Thread B)几乎同时调用了

getInstance()

方法。

Thread A 执行到

if (instance == null)

,发现

instance

确实是

null

。Thread B 也执行到

if (instance == null)

,同样发现

instance

是

null

(因为 Thread A 还没来得及创建并赋值)。Thread A 继续执行

instance = new SimpleSingleton();

,创建了一个实例。紧接着,Thread B 也执行

instance = new SimpleSingleton();

,又创建了一个实例。

瞧,原本我们希望只有一个实例,结果却在内存中拥有了两个甚至更多的

SimpleSingleton

对象。这不仅违背了单例模式的初衷,还可能导致一些难以预料的程序行为,比如资源冲突、状态不一致等。这就是所谓的“竞态条件”(Race Condition)问题,多个线程竞争共享资源(这里是

instance

的创建和赋值),导致结果不可预测。所以,对于任何需要在多线程环境中使用的单例,我们都必须认真考虑其线程安全性。

除了双重检查锁定,还有哪些实现线程安全单例的方法?各自的优缺点是什么?

当然有,双重检查锁定虽然高效,但也不是唯一的选择。在不同的场景和对性能、简洁性有不同要求时,我们会有其他考量。这里我列举几种常见的线程安全单例实现方式:

1. 饿汉式(Eager Initialization)

这是最简单直接的一种。在类加载的时候就直接创建实例。

public class EagerSingleton {    private static EagerSingleton instance = new EagerSingleton(); // 类加载时即创建    private EagerSingleton() {}    public static EagerSingleton getInstance() {        return instance;    }}

优点:天生线程安全: 由于实例在类加载时就创建了,JVM会保证这个过程是线程安全的,所以不存在并发问题。实现简单: 代码量少,容易理解。缺点:非懒加载: 无论这个单例实例是否会被用到,它都会在类加载时被创建。如果单例的初始化比较耗时,或者它占用的资源比较多,而程序运行期间又很少用到它,这就会造成资源的浪费。

2. 懒汉式加锁(Synchronized getInstance() Method)

这是在最简单懒汉式基础上,直接给

getInstance()

方法加上

synchronized

关键字。

public class SynchronizedSingleton {    private static SynchronizedSingleton instance;    private SynchronizedSingleton() {}    public static synchronized SynchronizedSingleton getInstance() { // 整个方法加锁        if (instance == null) {            instance = new SynchronizedSingleton();        }        return instance;    }}

优点:懒加载: 只有在第一次调用

getInstance()

时才会创建实例。线程安全:

synchronized

关键字确保了同一时间只有一个线程能进入

getInstance()

方法,从而保证了实例的唯一性。缺点:性能开销大: 每次调用

getInstance()

方法时,都需要进行同步,这会带来不小的性能损耗。即使实例已经创建,后续的每次调用依然需要获取和释放锁,这在并发量大的系统中是不可接受的。

3. 静态内部类(Static Inner Class / Initialization-on-demand holder idiom)

这是一种非常优雅且推荐的实现方式,被认为是Java中实现线程安全单例的最佳实践之一。

public class StaticInnerClassSingleton {    private StaticInnerClassSingleton() {}    // 静态内部类,只有在第一次使用时才会被加载    private static class SingletonHolder {        private static final StaticInnerClassSingleton INSTANCE = new StaticInnerClassSingleton();    }    public static StaticInnerClassSingleton getInstance() {        return SingletonHolder.INSTANCE;    }}

优点:懒加载:

SingletonHolder

这个静态内部类只有在

getInstance()

方法被调用时才会被加载,从而实现了实例的延迟初始化。线程安全: JVM在加载类时是线程安全的,它会保证

SingletonHolder

类只会被加载一次,并且在加载过程中创建

instance

实例。性能高:

getInstance()

方法本身没有同步块,所以每次调用都没有额外的性能开销。缺点:相较于饿汉式,代码稍微多一点点,但其带来的好处是显而易见的。

4. 枚举单例(Enum Singleton)

这是Java语言在JDK 1.5之后提供的一种实现单例的最佳方式,由Effective Java的作者Joshua Bloch推荐。

public enum EnumSingleton {    INSTANCE; // 唯一的实例    public void showMessage() {        System.out.println("Hello from the Enum Singleton!");    }}

优点:最简洁: 代码量最少。天生线程安全: 枚举类型在JVM层面就保证了其单例性,没有任何并发问题。防止反射攻击: 枚举没有公共构造器,所以无法通过反射创建多个实例。防止反序列化问题: 枚举实例的序列化和反序列化由JVM特殊处理,不会创建新的实例。缺点:不适用于所有场景: 如果你的单例需要继承其他类(Java枚举默认继承

Enum

),或者需要复杂的初始化逻辑,枚举单例可能就不太合适了。

在我看来,如果你使用的是Java,并且对单例的懒加载、线程安全和性能都有要求,那么静态内部类或者枚举单例通常是最好的选择。DCL虽然经典,但理解和正确实现需要更多细节考量(特别是

volatile

),稍有不慎就可能出错。

在使用双重检查锁定(DCL)时,

volatile

关键字到底起到了什么关键作用?

volatile

关键字在DCL中扮演的角色,简直就是整个模式的灵魂,少了它,DCL就可能失效,甚至引发非常隐晦且难以调试的错误。它的关键作用主要体现在两个方面:内存可见性和防止指令重排序。

我们先来理解一下,一个对象创建的过程,在JVM底层通常会分解成几个步骤:

分配内存: 为

ThreadSafeSingleton

对象分配一块内存空间。初始化对象: 调用

ThreadSafeSingleton

的构造函数,执行一些初始化操作,比如设置字段的默认值,或者执行构造函数中的业务逻辑。设置引用: 将

instance

变量指向刚刚分配的内存地址。

问题就出在这里。在没有

volatile

关键字修饰

instance

变量的情况下,JVM的编译器和CPU为了优化性能,可能会对这三个步骤进行指令重排序。也就是说,步骤2和步骤3的顺序可能会颠倒,变成1 -> 3 -> 2。

如果发生了这种重排序,我们设想一下这样的场景:

Thread A 进入

getInstance()

方法,通过了第一次

null

检查,进入同步块。Thread A 开始创建实例,但由于指令重排序,它先执行了步骤1(分配内存)和步骤3(设置引用),将

instance

指向了这块内存地址,但此时步骤2(对象初始化)还没有完成!也就是说,

instance

已经不为

null

了,但它指向的却是一个“半成品”对象。此时,Thread A 暂时被挂起(比如时间片用完)。Thread B 进入

getInstance()

方法,执行第一次

null

检查。它发现

instance

已经不为

null

了(因为它已经被Thread A指向了那块内存),于是Thread B直接返回了这个“半成品”的

instance

。Thread B 尝试使用这个

instance

对象,由于对象还没有完全初始化,它可能会访问到未初始化的字段,导致

NullPointerException

或其他不可预知的错误。

这就是

volatile

的第一个作用:防止指令重排序。当

instance

被

volatile

修饰后,JVM会保证在

instance = new ThreadSafeSingleton()

这行代码中,对象初始化(步骤2)一定会在

instance

变量被赋值(步骤3)之前完成。这确保了当其他线程看到

instance

不为

null

时,它所指向的对象一定是已经完全初始化好的。

volatile

的第二个作用是内存可见性。它确保了对

instance

变量的任何修改(比如赋值操作)都会立即被刷新到主内存中,并且其他线程在读取

instance

变量时,都会从主内存中重新读取,而不是使用自己线程工作内存中的旧值。这样就避免了一个线程修改了

instance

,而另一个线程却看不到这个修改,依然使用旧的

null

值,从而再次进入同步块创建新实例的问题。

所以,

volatile

在DCL中是不可或缺的,它像是给

instance

变量加了一层“契约”,保证了其在并发环境下的正确行为。没有它,DCL模式的线程安全性和可靠性就无从谈起。

以上就是如何实现一个线程安全的单例?的详细内容,更多请关注创想鸟其它相关文章!

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 chuangxiangniao@163.com 举报,一经查实,本站将立刻删除。
发布者:程序猿,转转请注明出处:https://www.chuangxiangniao.com/p/1370053.html

赞 (0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
Python的多线程和多进程有什么区别?如何选择?
上一篇 2025年12月14日 10:11:51
如何合并两个字典?
下一篇 2025年12月14日 10:12:00

相关推荐

  • 一周学会蝴蝶号无人直播的完整课程计划推荐

    一周学会蝴蝶号无人直播的完整课程计划推荐一周学会蝴蝶号无人直播的完整课程计划推荐一周学会蝴蝶号无人直播的完整课程计划推荐一周学会蝴蝶号无人直播的完整课程计划推荐

    掌握“蝴蝶号”无人直播的核心要义,一周内可搭建初步系统并具备独立操作能力。1.第一天厘清概念并完成基础环境搭建;2.第二天熟悉obs基础操作与场景构建;3.第三天准备高质量内容素材并确定风格;4.第四天设置自动化逻辑与推流配置;5.第五天处理互动机制及常见问题;6.第六天进行首次正式直播并复盘;7.…

    2026年9月21日 • 用户投稿
    100
  • 使用EventBus实现Android实时速度显示与后台保存教程

    本教程详细介绍了如何在Android应用中实现实时速度的显示与后台保存功能。通过利用前台服务(Foreground Service)获取位置数据,并结合EventBus库实现服务与UI界面(MainActivity)之间的实时数据通信,确保即使应用处于后台或屏幕关闭时,速度数据也能持续更新并显示在用…

    2026年9月21日
    000
  • 在Java中如何分析异常堆栈性能开销

    异常堆栈在高并发场景下开销显著,因JVM需遍历调用栈、创建对象、字符串拼接及同步操作,频繁使用将增加GC压力与CPU消耗;可通过JMH测试量化影响,发现填充堆栈耗时可达清空的10倍以上;建议避免在热点代码抛异常、禁用非必要堆栈填充、按需打印日志、使用异步日志框架,并借助JFR、Profiler和GC…

    2026年9月21日
    000
  • Java ConcurrentSkipListMap在并发场景下应用

    ConcurrentSkipListMap是基于跳跃表实现的线程安全有序映射,支持高并发读写与高效范围查询,适用于需排序的并发场景,如排行榜系统;相比ConcurrentHashMap,它提供有序性与导航操作,但插入查找为O(log n),内存开销较大,适合读多写少或需区间扫描的业务。 在高并发场景…

    2026年9月21日
    100
  • VSCode怎么启动Layui项目_VSCode运行Layui前端框架项目教程

    必须使用本地服务器运行Layui项目,因为直接打开HTML文件通过file://协议会受浏览器安全限制,导致AJAX、跨域等功能异常,Layui组件无法正常加载;推荐安装Node.js后使用npm全局安装http-server,通过命令行启动服务,或在VSCode中安装Live Server插件,右…

    2026年9月21日
    000
  • Java 正则表达式:查找双引号内所有指定字符串的出现次数

    本文旨在解决在 Java 中使用正则表达式查找双引号内特定字符串(例如 “variant”)的所有出现次数的问题。我们将提供一个完整的解决方案,包括正则表达式的构建、代码示例以及详细的解释,帮助开发者准确高效地完成此类任务。 在 Java 中,使用正则表达式查找字符串中特定模…

    2026年9月21日
    000
  • MySQL 大型历史数据表结构设计与优化指南

    本文旨在为处理大量客户历史交易数据的MySQL数据库设计提供专业指导。我们将探讨如何构建高效、可扩展的表结构,重点关注主键设计、数据分区、实时数据摄入以及性能优化策略,以确保系统能够稳定支持百万级乃至亿级数据量的查询需求。 MySQL大型历史数据表结构设计与优化 在处理大量历史数据,特别是涉及到多用…

    2026年9月21日
    000
  • 如何用PyTorch训练AI大模型?构建高效神经网络的完整教程

    如何用PyTorch训练AI大模型?构建高效神经网络的完整教程如何用PyTorch训练AI大模型?构建高效神经网络的完整教程如何用PyTorch训练AI大模型?构建高效神经网络的完整教程如何用PyTorch训练AI大模型?构建高效神经网络的完整教程

    PyTorch大模型训练需综合运用分布式训练、内存优化与高效计算策略。首先采用DistributedDataParallel实现多GPU并行,配合DistributedSampler确保数据均衡;通过混合精度训练、梯度累积和激活检查点缓解显存压力;使用torch.compile优化模型计算效率;选择…

    2026年9月21日 • 用户投稿
    100
  • Spring Boot异常处理:为何需要自定义异常而非仅依赖HTTP状态码

    在Spring Boot应用中,自定义异常提供了比单一HTTP状态码更丰富的错误上下文,能够更精确地传达问题根源。这种细粒度的异常处理不仅提升了代码的可读性和可维护性,也极大地改善了用户体验,使客户端能够基于具体错误类型做出智能响应,而非仅仅接收到一个模糊的状态码。 为什么需要自定义异常? 在构建r…

    2026年9月21日
    200
  • 百家号视频怎么隐藏?百家号怎么设置仅自己可见

    随着短视频平台的快速发展,其已成为人们获取资讯和休闲娱乐的重要方式。作为国内知名的自媒体平台之一,百家号吸引了大量用户。然而,在享受便捷的同时,隐私安全问题也日益突出。本文将介绍百家号视频隐藏的方法,帮助用户更好地保护个人内容,维护隐私安全。 一、百家号视频隐藏方法 设置隐私权限 在百家号后台,用户…

    2026年9月21日
    200
  • Java Random类如何生成随机数

    Random类位于java.util包,通过实例化生成伪随机数;无参构造以系统时间作种子,带参构造用固定种子可复现序列;提供nextInt()、nextDouble()等方法生成不同类型随机值;指定范围整数可用rand.nextInt(max-min)+min实现;多线程推荐ThreadLocalR…

    2026年9月21日
    200
  • VSCode怎么改环境_VSCode切换Python/Node等多版本环境教程

    切换VSCode环境需先安装对应语言扩展,再通过命令面板选择解释器或使用nvm切换Node版本,配合虚拟环境或launch.json配置确保运行和调试时使用正确版本,可通过终端命令验证环境,若失效可检查缓存、扩展冲突或权限问题。 VSCode改环境,其实就是让VSCode知道你想用哪个版本的Pyth…

    2026年9月21日
    100
  • Sublime开发MySQL存储过程教程实战_封装重复逻辑减少前端负担

    Sublime开发MySQL存储过程教程实战_封装重复逻辑减少前端负担Sublime开发MySQL存储过程教程实战_封装重复逻辑减少前端负担Sublime开发MySQL存储过程教程实战_封装重复逻辑减少前端负担Sublime开发MySQL存储过程教程实战_封装重复逻辑减少前端负担

    在web开发中使用mysql存储过程能有效封装逻辑并减少前端负担,本文介绍了其优势、环境配置及实战技巧。一、存储过程的优势包括减少网络传输、提高性能、统一业务逻辑;二、sublime text配置步骤为安装package control、sublimerepl插件、sql语法高亮插件,并建议新建.s…

    2026年9月21日 • 用户投稿
    900
  • Java中字符到数字转换:解决for循环提前返回的常见陷阱

    本文探讨java中`for`循环在字符到数字转换时,因`return`语句放置不当导致程序提前终止、无法完整处理字符串的问题。我们将分析这种常见陷阱,并提供修正方案,演示如何正确利用循环填充数组,并在循环结束后统一返回最终结果,确保每个字符都能被准确映射和组合。 引言:字符到数字的映射需求 在编程实…

    2026年9月21日
    100
  • 梦幻号虚拟主播电商运营宝典(附新手教程+配套工具清单)

    虚拟主播电商的核心在于“内容驱动销售,人设凝聚用户”,要让“梦幻号”真正动起来并实现带货,必须先赋予其鲜明的人设,包括清晰的定位标签(如美食家、科技宅)、独特的人格魅力(性格、口头禅、小缺点)和与产品的强关联性,使其具备辨识度和故事感,从而建立用户信任;接着通过obs studio、vtube st…

    2026年9月21日
    100
  • 软删除(Soft Delete)的实现与恢复逻辑

    使用软删除的原因是它允许数据恢复和保持数据完整性。1) 软删除通过标记数据为已删除而非实际删除,提供了数据恢复的可能性。2) 它保持数据的历史记录,确保数据完整性。实现软删除通常在数据库中添加字段如is_deleted或deleted_at,恢复数据时重置这些字段。 软删除(Soft Delete)…

    2026年9月21日
    100
  • Java多线程API调用中Future.get()返回null的解决方案

    本文旨在解决%ignore_a_1%api调用中`future.get()`方法返回`null`的常见问题。当使用`callable`和`executorservice`并发执行api请求并尝试获取结果时,如果流读取逻辑不当,可能导致获取到的数据为空。文章将详细解释问题根源,并提供使用`string…

    2026年9月21日
    100
  • 升级后如何检查兼容性

    检查兼容性是升级后确保系统稳定的关键,需先确认硬件配置与驱动支持,再验证软件运行及业务流程正常,最后通过系统日志排查潜在错误,逐步排除风险。 系统或软件升级后,检查兼容性是确保各项功能正常运行的关键步骤。直接进入实际使用前,花时间验证兼容性可以避免数据丢失、服务中断等问题。 检查硬件和驱动支持 某些…

    2026年9月21日
    100
  • 小红书零基础赚钱攻略(精准选题+涨粉秘籍+账号运营+高转化变现方法)

    找到自己真正擅长或有热情的领域,结合用户需求和竞争情况确定细分赛道;2. 通过优质内容、高互动数据、精准关键词和话题标签提升曝光;3. 利用品牌合作、带货佣金、知识付费等方式实现变现,核心是建立在信任基础上的持续价值输出,最终将流量转化为实际收益。 小红书零基础赚钱,核心在于找到自己的定位,持续输出…

    2026年9月21日
    100
  • .com网站安全维护_保障.com网站稳定的措施

    答案:保障.com网站稳定需加强安全防护、定期备份、实时监控和应急准备。部署防火墙、更新系统、使用HTTPS、限制端口;制定自动备份并异地存储,定期恢复测试;利用监控工具检测可用性与异常流量,优化加载速度;建立应急流程,严格权限管理,定期演练。细节执行到位才能确保长期安全稳定运行。 确保.com网站…

    2026年9月21日
    200

发表回复

登录后才能评论
关注微信