【JAVA探索之路】从头开始讲透、实现单例模式

目录

一、前言

二、什么是单例模式

三、实现单例模式多种版本

1.懒汉式写法

2.饿汉式写法

[3.双重检查锁 DCL](#3.双重检查锁 DCL)

4.静态内部类

四、单例模式会被怎么破坏

[1. 反射破坏](#1. 反射破坏)

[2. 序列化破坏](#2. 序列化破坏)

五、单例模式总结


一、前言

单例模式几乎是 Java 面试里的**"常驻嘉宾"**。很多人会背几种写法,但一旦面试官继续追问"为什么线程不安全""为什么要加 volatile""静态内部类为什么可行",就容易卡住。

这篇文章不只讲"怎么写",更想讲清楚"为什么这么写"。我们从最基础的实现开始,一步步手撕到线程安全、性能优化,以及反射和序列化这些高频追问。


二、什么是单例模式

单例模式,顾名思义,就是一个类在整个系统中只允许存在一个实例,并且提供一个全局访问入口。

它的核心目标只有两个:

  1. 保证唯一实例
  2. 提供统一访问方式

常见场景包括:

  • 配置中心
  • 日志组件
  • 数据库连接管理器
  • 缓存管理器
  • 线程池管理器

听起来很简单,但真正难的地方在于:既要保证只有一个对象,又要在并发环境下安全,还希望性能别太差。

一个标准的单例,通常要满足:

  1. 构造器私有化,防止外部 new
  2. 类内部自己持有唯一实例
  3. 对外提供获取实例的方法

最基本的结构如下:

java 复制代码
public class Singleton {
    private static Singleton instance;

    private Singleton() {
    }

    public static Singleton getInstance() {
        return instance;
    }
}

三、实现单例模式多种版本

1.懒汉式写法

懒汉式,最容易写,也最容易出问题,思路是:对象先不创建,等第一次用到时再创建。

java 复制代码
public class Singleton {
    private static Singleton instance;

    private Singleton() {
    }

    public static Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}

优点

  • 延迟加载
  • 写法直观

缺点

  • 线程不安全

为什么线程不安全?

假设线程 A 和线程 B 同时进入 getInstance(),并且都判断 instance == null 成立,那么它们都会执行 new Singleton(),最终就可能创建出多个对象。

所以,这一版只能在单线程环境下用,面试里如果只写到这里,基本一定会被追问。

也可以在getInstance()上加锁防止线程安全问题

2.饿汉式写法

饿汉式,简单粗暴,天然线程安全,思路和懒汉式相反:类加载时就直接把实例创建好。

java 复制代码
public class Singleton {
    private static final Singleton INSTANCE = new Singleton();

    private Singleton() {
    }

    public static Singleton getInstance() {
        return INSTANCE;
    }
}

优点

  • 实现简单
  • 天然线程安全
  • 调用性能好

为什么线程安全?

因为类加载过程本身就是线程安全的,JVM 会保证类只加载一次,所以静态实例也只会初始化一次。

缺点

  • 没有延迟加载
  • 如果这个对象一直没被用到,就会造成一定资源浪费

适用场景

如果这个单例对象本身很轻量,或者项目启动后大概率一定会用到,那饿汉式完全没问题。

3.双重检查锁 DCL

为了兼顾线程安全和性能,很多人会想到:既然每次都加锁太重,那能不能只在第一次创建时加锁?

这就是双重检查锁。

java 复制代码
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;
    }
}

为什么要判空两次

第一次 if (instance == null):

  • 避免每次都进入同步块
  • 提高性能

第二次 if (instance == null):

  • 防止多个线程进入第一层判断后,重复创建对象

为什么 volatile 不能少

这部分是面试最爱问的点。

new Singleton() 这行代码看起来像一个原子操作,但实际上底层大致会经历三步:

  1. 分配内存
  2. 初始化对象
  3. 将引用赋值给 instance

问题在于,JVM 可能发生指令重排,变成:

  1. 分配内存
  2. 将引用赋值给 instance
  3. 初始化对象

如果线程 A 执行到第 2 步,此时线程 B 进来发现 instance != null,就直接返回了一个"还没初始化完成"的对象,这就会出问题。

volatile 的作用就是:

  • 禁止指令重排
  • 保证可见性

所以,DCL 必须搭配 volatile 使用,否则就是不完整写法。

4.静态内部类

很多时候,工程里更推荐静态内部类写法。

java 复制代码
public class Singleton {
    private Singleton() {
    }

    private static class Holder {
        private static final Singleton INSTANCE = new Singleton();
    }

    public static Singleton getInstance() {
        return Holder.INSTANCE;
    }
}

为什么它线程安全

因为静态内部类 Holder 不会在外部类加载时立即加载,只有第一次调用 getInstance() 时,才会触发 Holder 的加载和初始化。

而类加载过程又是线程安全的,因此它天然保证了:

  • 延迟加载
  • 线程安全
  • 不需要显式加锁

优点

  • 写法简洁
  • 性能好
  • 延迟加载
  • 线程安全

很多场景下,这一版是比 DCL 更推荐的选择。


四、单例模式会被怎么破坏

很多人以为把构造器私有化就万无一失了,其实不够。

1. 反射破坏

即使构造器是 private,也可以通过反射强行访问。

java 复制代码
Constructor<Singleton> constructor = Singleton.class.getDeclaredConstructor();
constructor.setAccessible(true);
Singleton s1 = constructor.newInstance();
Singleton s2 = constructor.newInstance();

System.out.println(s1 == s2); // false

如何防御

可以在构造器里增加判断:

java 复制代码
public class Singleton {
    private static volatile Singleton instance;

    private Singleton() {
        if (instance != null) {
            throw new RuntimeException("Singleton already exists");
        }
    }

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

2. 序列化破坏

如果单例类实现了 Serializable,序列化再反序列化后,可能得到一个新对象。

java 复制代码
Singleton s1 = Singleton.getInstance();
// 序列化 s1
// 反序列化得到 s2
System.out.println(s1 == s2); // 可能是 false

如何防御

实现 readResolve():

java 复制代码
public class Singleton implements Serializable {
    private static final Singleton INSTANCE = new Singleton();

    private Singleton() {
    }

    public static Singleton getInstance() {
        return INSTANCE;
    }

    private Object readResolve() {
        return INSTANCE;
    }
}

这样反序列化时,返回的仍然是原来的单例对象。


五、单例模式总结

单例模式表面上只是"让一个类只能创建一个对象",但真正的难点在于并发安全和边界问题。

我们可以把它理解成三个层次:

  1. 会写单例
  2. 写对线程安全的单例
  3. 理解为什么这样写,以及它还会被什么方式破坏

如果只让我给一个工程里比较推荐的版本,我会优先选静态内部类:

java 复制代码
public class Singleton {
    private Singleton() {
    }

    private static class Holder {
        private static final Singleton INSTANCE = new Singleton();
    }

    public static Singleton getInstance() {
        return Holder.INSTANCE;
    }
}

单例模式真正要掌握的,不是背下哪几段代码,而是明白:

  • 为什么懒汉式会出并发问题
  • 为什么 DCL 要配 volatile
  • 为什么类加载机制能保证线程安全
  • 为什么反射和序列化可能破坏单例

制作不易,如果对你有帮助请**点赞,评论,收藏,**感谢大家的支持

相关推荐
阿正的梦工坊7 小时前
JavaScript 微任务与宏任务完全指南
开发语言·javascript·ecmascript
chools7 小时前
【AI超级智能体】快速搞懂工具调用Tool Calling 和 MCP协议
java·人工智能·学习·ai
知行合一。。。7 小时前
Python--05--面向对象(属性,方法)
android·开发语言·python
李白你好7 小时前
TongWeb EJB 反序列化生成工具(Java-Chain 插件)
java·安全
青梅橘子皮8 小时前
C语言---指针的应用以及一些面试题
c语言·开发语言·算法
浅时光_c8 小时前
3 shell脚本编程
linux·开发语言·bash
Evand J8 小时前
【三维轨迹目标定位,CKF+RTS,MATLAB程序】基于CKF与RTS平滑的三维非线性目标跟踪(距离+方位角+俯仰角)
开发语言·matlab·目标跟踪
U盘失踪了8 小时前
Java 的 JAR 是什么?
java·jar
今天又在写代码9 小时前
java-v2
java·开发语言