0

0

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

紅蓮之龍

紅蓮之龍

发布时间:2025-09-04 18:23:01

|

297人浏览过

|

来源于php中文网

原创

答案:双重检查锁定(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()
方法。

  1. Thread A 执行到
    if (instance == null)
    ,发现
    instance
    确实是
    null
  2. Thread B 也执行到
    if (instance == null)
    ,同样发现
    instance
    null
    (因为 Thread A 还没来得及创建并赋值)。
  3. Thread A 继续执行
    instance = new SimpleSingleton();
    ,创建了一个实例。
  4. 紧接着,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)

华友协同办公自动化OA系统
华友协同办公自动化OA系统

华友协同办公管理系统(华友OA),基于微软最新的.net 2.0平台和SQL Server数据库,集成强大的Ajax技术,采用多层分布式架构,实现统一办公平台,功能强大、价格便宜,是适用于企事业单位的通用型网络协同办公系统。 系统秉承协同办公的思想,集成即时通讯、日记管理、通知管理、邮件管理、新闻、考勤管理、短信管理、个人文件柜、日程安排、工作计划、工作日清、通讯录、公文流转、论坛、在线调查、

下载

这是一种非常优雅且推荐的实现方式,被认为是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底层通常会分解成几个步骤:

  1. 分配内存:
    ThreadSafeSingleton
    对象分配一块内存空间。
  2. 初始化对象: 调用
    ThreadSafeSingleton
    的构造函数,执行一些初始化操作,比如设置字段的默认值,或者执行构造函数中的业务逻辑。
  3. 设置引用:
    instance
    变量指向刚刚分配的内存地址。

问题就出在这里。在没有

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

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

  1. Thread A 进入
    getInstance()
    方法,通过了第一次
    null
    检查,进入同步块。
  2. Thread A 开始创建实例,但由于指令重排序,它先执行了步骤1(分配内存)和步骤3(设置引用),将
    instance
    指向了这块内存地址,但此时步骤2(对象初始化)还没有完成!也就是说,
    instance
    已经不为
    null
    了,但它指向的却是一个“半成品”对象。
  3. 此时,Thread A 暂时被挂起(比如时间片用完)。
  4. Thread B 进入
    getInstance()
    方法,执行第一次
    null
    检查。它发现
    instance
    已经不为
    null
    了(因为它已经被Thread A指向了那块内存),于是Thread B直接返回了这个“半成品”的
    instance
  5. 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模式的线程安全性和可靠性就无从谈起。

热门AI工具

更多
DeepSeek
DeepSeek

幻方量化公司旗下的开源大模型平台

豆包大模型
豆包大模型

字节跳动自主研发的一系列大型语言模型

通义千问
通义千问

阿里巴巴推出的全能AI助手

腾讯元宝
腾讯元宝

腾讯混元平台推出的AI助手

文心一言
文心一言

文心一言是百度开发的AI聊天机器人,通过对话可以生成各种形式的内容。

讯飞写作
讯飞写作

基于讯飞星火大模型的AI写作工具,可以快速生成新闻稿件、品宣文案、工作总结、心得体会等各种文文稿

即梦AI
即梦AI

一站式AI创作平台,免费AI图片和视频生成。

ChatGPT
ChatGPT

最最强大的AI聊天机器人程序,ChatGPT不单是聊天机器人,还能进行撰写邮件、视频脚本、文案、翻译、代码等任务。

相关专题

更多
c语言中null和NULL的区别
c语言中null和NULL的区别

c语言中null和NULL的区别是:null是C语言中的一个宏定义,通常用来表示一个空指针,可以用于初始化指针变量,或者在条件语句中判断指针是否为空;NULL是C语言中的一个预定义常量,通常用来表示一个空值,用于表示一个空的指针、空的指针数组或者空的结构体指针。

235

2023.09.22

java中null的用法
java中null的用法

在Java中,null表示一个引用类型的变量不指向任何对象。可以将null赋值给任何引用类型的变量,包括类、接口、数组、字符串等。想了解更多null的相关内容,可以阅读本专题下面的文章。

438

2024.03.01

if什么意思
if什么意思

if的意思是“如果”的条件。它是一个用于引导条件语句的关键词,用于根据特定条件的真假情况来执行不同的代码块。本专题提供if什么意思的相关文章,供大家免费阅读。

775

2023.08.22

c++怎么把double转成int
c++怎么把double转成int

本专题整合了 c++ double相关教程,阅读专题下面的文章了解更多详细内容。

73

2025.08.29

C++中int、float和double的区别
C++中int、float和double的区别

本专题整合了c++中int和double的区别,阅读专题下面的文章了解更多详细内容。

101

2025.10.23

c++中volatile关键字的作用
c++中volatile关键字的作用

本专题整合了c++中volatile关键字的相关内容,阅读专题下面的文章了解更多详细内容。

69

2025.10.23

class在c语言中的意思
class在c语言中的意思

在C语言中,"class" 是一个关键字,用于定义一个类。想了解更多class的相关内容,可以阅读本专题下面的文章。

469

2024.01.03

python中class的含义
python中class的含义

本专题整合了python中class的相关内容,阅读专题下面的文章了解更多详细内容。

13

2025.12.06

Python 自然语言处理(NLP)基础与实战
Python 自然语言处理(NLP)基础与实战

本专题系统讲解 Python 在自然语言处理(NLP)领域的基础方法与实战应用,涵盖文本预处理(分词、去停用词)、词性标注、命名实体识别、关键词提取、情感分析,以及常用 NLP 库(NLTK、spaCy)的核心用法。通过真实文本案例,帮助学习者掌握 使用 Python 进行文本分析与语言数据处理的完整流程,适用于内容分析、舆情监测与智能文本应用场景。

10

2026.01.27

热门下载

更多
网站特效
/
网站源码
/
网站素材
/
前端模板

精品课程

更多
相关推荐
/
热门推荐
/
最新课程
Rust 教程
Rust 教程

共28课时 | 4.9万人学习

JavaScript
JavaScript

共185课时 | 20.9万人学习

HTML教程
HTML教程

共500课时 | 5.1万人学习

关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送

Copyright 2014-2026 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号