当前位置: 代码网 > it编程>编程语言>Java > JAVA从头开始讲透、实现单例模式的探索之路

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

2026年05月12日 Java 我要评论
一、前言单例模式几乎是 java 面试里的“常驻嘉宾”。很多人会背几种写法,但一旦面试官继续追问“为什么线程不安全”“为什么要加volati

一、前言

单例模式几乎是 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单例模式内容请搜索代码网以前的文章或继续浏览下面的相关文章希望大家以后多多支持代码网!

(0)

相关文章:

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

发表评论

验证码:
Copyright © 2017-2026  代码网 保留所有权利. 粤ICP备2024248653号
站长QQ:2386932994 | 联系邮箱:2386932994@qq.com