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

 更新时间:2026年05月12日 08:56:36   作者:晔子yy  
java单例模式是一种常见的设计模式,在它的核心结构中只包含一个被称为单例的特殊类,下面这篇文章主要介绍了JAVA从头开始讲透、实现单例模式的相关资料,文中通过代码介绍的非常详细,需要的朋友可以参考下

一、前言

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

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

二、什么是单例模式

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

它的核心目标只有两个:

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

常见场景包括:

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

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

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

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

最基本的结构如下:

public class Singleton {
    private static Singleton instance;

    private Singleton() {
    }

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

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

1.懒汉式写法

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

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.饿汉式写法

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

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

    private Singleton() {
    }

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

优点

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

为什么线程安全?

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

缺点

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

适用场景

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

3.双重检查锁 DCL

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

这就是双重检查锁。

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.静态内部类

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

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,也可以通过反射强行访问。

Constructor<Singleton> constructor = Singleton.class.getDeclaredConstructor();
constructor.setAccessible(true);
Singleton s1 = constructor.newInstance();
Singleton s2 = constructor.newInstance();

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

如何防御

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

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,序列化再反序列化后,可能得到一个新对象。

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

如何防御

实现 readResolve():

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. 理解为什么这样写,以及它还会被什么方式破坏

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

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
  • 为什么类加载机制能保证线程安全
  • 为什么反射和序列化可能破坏单例

到此这篇关于JAVA实现单例模式的文章就介绍到这了,更多相关JAVA单例模式内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • Spring注解方式无法扫描Service注解的解决

    Spring注解方式无法扫描Service注解的解决

    这篇文章主要介绍了Spring注解方式无法扫描Service注解的解决方案,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2021-10-10
  • Java日期时间格式化操作DateUtils 的整理

    Java日期时间格式化操作DateUtils 的整理

    这篇文章主要介绍了Java日期时间格式化操作DateUtils 的整理的相关资料,这里总结了java日期格式化的操作,需要的朋友可以参考下
    2017-07-07
  • 每天学Java!一分钟了解JRE与JDK

    每天学Java!一分钟了解JRE与JDK

    每天学Java!一分钟了解JRE与JDK,什么是JRE?什么是JDK?什么是JVM?相信通过本文大家都会有所了解,感兴趣的小伙伴们可以参考一下
    2016-07-07
  • Java中局部变量和成员变量的区别详解

    Java中局部变量和成员变量的区别详解

    这篇文章主要介绍了Java中局部变量和成员变量的区别,本文将通过示例为大家详细讲讲Java中成员变量与局部变量之间的区别,感兴趣的同学可以了解一下
    2023-05-05
  • 如何合理管控Java语言的异常

    如何合理管控Java语言的异常

    这篇文章主要介绍了如何合理管控Java语言的异常问题,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教
    2025-05-05
  • Http服务与Dubbo服务相互转换的SpringBoot代理节点实现方式

    Http服务与Dubbo服务相互转换的SpringBoot代理节点实现方式

    文章主要讨论如何在项目中增加一个SpringBoot节点,作为HTTP与Dubbo服务之间的代理节点,该节点通过注册到Eureka,提供SpringCloud服务,并支持Dubbo代理Bean的管理,文章提到使用io.dubbo:spring-boot-starter-dubbo依赖,但遇到消费者配置生产者的问题
    2025-10-10
  • IntelliJ IDEA 2019.3激活破解的详细方法(亲测有效,可激活至 2089 年)

    IntelliJ IDEA 2019.3激活破解的详细方法(亲测有效,可激活至 2089&

    本教程适用于 JetBrains 全系列产品,包括 Pycharm、IDEA、WebStorm、Phpstorm、Datagrip、RubyMine、CLion、AppCode 等,本教程无需修改 hosts 文件,对IntelliJ IDEA 2019.3激活破解的详细方法的相关知识感兴趣的朋友一起看看吧
    2020-09-09
  • SpringBoot Controller返回图片的三种方式

    SpringBoot Controller返回图片的三种方式

    在互联网的世界里,图片无处不在,它们是信息传递的重要媒介,也是视觉盛宴的一部分,而在Spring Boot项目中,如何优雅地处理和返回图片数据,则成为了开发者们不得不面对的问题,今天,就让我们一起来探索Spring Boot Controller的神奇转换,需要的朋友可以参考下
    2024-07-07
  • maven实现docker自动化部署插件的使用

    maven实现docker自动化部署插件的使用

    本文主要介绍了maven实现docker自动化部署插件的使用,分享给大家,感兴趣的小伙伴们可以参考一下
    2021-06-06
  • java构建一个BigDecimal数字格式化工具

    java构建一个BigDecimal数字格式化工具

    这篇文章主要为大家详细介绍了如何使用java创建一个BigDecimal格式化工具,实现将数字格式化为"#,###,##0.00"格式,希望对大家有所帮助
    2025-11-11

最新评论