千家信息网

如何使用单例模式

发表于:2026-11-03 作者:千家信息网编辑
千家信息网最后更新 2026年11月03日,这篇文章主要讲解了"如何使用单例模式",文中的讲解内容简单清晰,易于学习与理解,下面请大家跟着小编的思路慢慢深入,一起来研究和学习"如何使用单例模式"吧!饿汉式饿汉式是最常见的也是最不需要考虑太多的单
千家信息网最后更新 2026年11月03日如何使用单例模式

这篇文章主要讲解了"如何使用单例模式",文中的讲解内容简单清晰,易于学习与理解,下面请大家跟着小编的思路慢慢深入,一起来研究和学习"如何使用单例模式"吧!

饿汉式

饿汉式是最常见的也是最不需要考虑太多的单例模式,因为他不存在线程安全问题,饿汉式也就是在类被加载的时候就创建实例对象。饿汉式的写法如下:

public class SingletonHungry {     private static SingletonHungry instance = new SingletonHungry();      private SingletonHungry() {     }      private static SingletonHungry getInstance() {         return instance;     } }
  • 测试代码如下:

class A {     public static void main(String[] args) {         IntStream.rangeClosed(1, 5)                 .forEach(i -> {                     new Thread(                             () -> {                                 SingletonHungry instance = SingletonHungry.getInstance();                                 System.out.println("instance = " + instance);                             }                     ).start();                 });     } }

结果

优点:线程安全,不需要关心并发问题,写法也是最简单的。

缺点:在类被加载的时候对象就会被创建,也就是说不管你是不是用到该对象,此对象都会被创建,浪费内存空间

懒汉式

以下是最基本的饿汉式的写法,在单线程情况下,这种方式是非常完美的,但是我们实际程序执行基本都不可能是单线程的,所以这种写法必定会存在线程安全问题

public class SingletonLazy {     private SingletonLazy() {     }      private static SingletonLazy instance = null;      public static SingletonLazy getInstance() {         if (null == instance) {             return new SingletonLazy();         }         return instance;      } }

演示多线程执行

class B {     public static void main(String[] args) {         IntStream.rangeClosed(1, 5)                 .forEach(i -> {                     new Thread(                             () -> {                                 SingletonLazy instance = SingletonLazy.getInstance();                                 System.out.println("instance = " + instance);                             }                     ).start();                 });     } }

结果

结果很显然,获取的实例对象不是单例的。也就是说这种写法不是线程安全的,也就不能在多线程情况下使用

DCL(双重检查锁式)

DCL 即 Double Check Lock 就是在创建实例的时候进行双重检查,首先检查实例对象是否为空,如果不为空将当前类上锁,然后再判断一次该实例是否为空,如果仍然为空就创建该是实例;代码如下:

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

测试代码如下:

class C {     public static void main(String[] args) {         IntStream.rangeClosed(1, 5)                 .forEach(i -> {                     new Thread(                             () -> {                                 SingleTonDcl instance = SingleTonDcl.getInstance();                                 System.out.println("instance = " + instance);                             }                     ).start();                 });     } }

结果

相信大多数初学者在接触到这种写法的时候已经感觉是「高大上」了,首先是判断实例对象是否为空,如果为空那么就将该对象的 Class 作为锁,这样保证同一时刻只能有一个线程进行访问,然后再次判断实例对象是否为空,最后才会真正的去初始化创建该实例对象。一切看起来似乎已经没有破绽,但是当你学过JVM后你可能就会一眼看出猫腻了。没错,问题就在 instance = new SingleTonDcl(); 因为这不是一个原子的操作,这句话的执行是在 JVM 层面分以下三步:

1.给 SingleTonDcl 分配内存空间 2.初始化 SingleTonDcl 实例 3.将 instance 对象指向分配的内存空间( instance 为 null 了)

正常情况下上面三步是顺序执行的,但是实际上JVM可能会「自作多情」得将我们的代码进行优化,可能执行的顺序是1、3、2,如下代码所示

public static SingleTonDcl getInstance() {     if (null == instance) {         synchronized (SingleTonDcl.class) {             if (null == instance) {                 1. 给 SingleTonDcl 分配内存空间                 3.将 instance 对象指向分配的内存空间( instance 不为 null 了)                 2. 初始化 SingleTonDcl 实例             }         }     }     return instance; }

假设现在有两个线程 t1, t2

  1. 鸿蒙官方战略合作共建--HarmonyOS技术社区

  2. 如果 t1 执行到以上步骤 3 被挂起

  3. 然后 t2 进入了 getInstance 方法,由于 t1 执行了步骤 3,此时的 instance 已经不为空了,所以 if (null == instance) 这个条件不为空,直接返回 instance, 但由于 t1 还未执行步骤 2,导致此时的 instance 实际上是个半成品,会导致不可预知的风险!

该怎么解决呢,既然问题出在指令有可能重排序上,不让它重排序不就行了,volatile 不就是干这事的吗,我们可以在 instance 变量前面加上一个 volatile 修饰符

画外音:volatile 的作用 1.保证的对象内存可见性 2.防止指令重排序

优化后的代码如下

public class SingleTonDcl {     private SingleTonDcl() {     }      //在对象前面添加 volatile 关键字即可     volatile private static SingleTonDcl instance = null;      public static SingleTonDcl getInstance() {         if (null == instance) {             synchronized (SingleTonDcl.class) {                 if (null == instance) {                     instance = new SingleTonDcl();                 }             }         }         return instance;     } }

到这里似乎问题已经解决了,双重锁机制 + volatile 实际上确实基本上解决了线程安全问题,保证了"真正"的单例。但真的是这样的吗?继续往下看

静态内部类

先看代码

public class SingleTonStaticInnerClass {     private SingleTonStaticInnerClass() {      }      private static class HandlerInstance {         private static SingleTonStaticInnerClass instance = new SingleTonStaticInnerClass();     }      public static SingleTonStaticInnerClass getInstance() {         return HandlerInstance.instance;     } }
  • 测试代码如下:

class D {     public static void main(String[] args) {         IntStream.rangeClosed(1, 5)                 .forEach(i->{                     new Thread(()->{                         SingleTonStaticInnerClass instance = SingleTonStaticInnerClass.getInstance();                         System.out.println("instance = " + instance);                     }).start();                 });     } }

静态内部类的特点:

这种写法使用 JVM 类加载机制保证了线程安全问题;由于 SingleTonStaticInnerClass 是私有的,除了 getInstance() 之外没有办法访问它,因此它是懒汉式的;同时读取实例的时候不会进行同步,没有性能缺陷;也不依赖 JDK 版本;

但是,它依旧不是完美的。

不安全的单例

上面实现单例都不是完美的,主要有两个原因

1. 反射攻击

首先要提到 java 中让人又爱又恨的反射机制, 闲言少叙,我们直接边上代码边说明,这里就以 DCL 举例(为什么选择 DCL 因为很多人觉得 DCL 写法是最高大上的....这里就开始去"打他们的脸")

将上面的 DCl 的测试代码修改如下:

class C {     public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {         Class singleTonDclClass = SingleTonDcl.class;         //获取类的构造器         Constructor constructor = singleTonDclClass.getDeclaredConstructor();         //把构造器私有权限放开         constructor.setAccessible(true);         //反射创建实例   注意反射创建要放在前面,才会攻击成功,因为如果反射攻击在后面,先使用正常的方式创建实例的话,在构造器中判断是可以防止反射攻击、抛出异常的,         //因为先使用正常的方式已经创建了实例,会进入if         SingleTonDcl instance = constructor.newInstance();         //正常的获取实例方式   正常的方式放在反射创建实例后面,这样当反射创建成功后,单例对象中的引用其实还是空的,反射攻击才能成功         SingleTonDcl instance1 = SingleTonDcl.getInstance();         System.out.println("instance1 = " + instance1);         System.out.println("instance = " + instance);     } }

居然是两个对象!内心是不是异常平静?果然和你想的不一样?其他的方式基本类似,都可以通过反射破坏单例。

2. 序列化攻击

我们以「饿汉式单例」为例来演示一下序列化和反序列化攻击代码,首先给饿汉式单例对应的类添加实现 Serializable 接口的代码,

public class SingletonHungry implements Serializable {     private static SingletonHungry instance = new SingletonHungry();      private SingletonHungry() {     }      private static SingletonHungry getInstance() {         return instance;     } }

然后看看如何使用序列化和反序列化进行攻击

SingletonHungry instance = SingletonHungry.getInstance(); ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("singleton_file"))); // 序列化【写】操作 oos.writeObject(instance); File file = new File("singleton_file"); ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file)) // 反序列化【读】操作 SingletonHungry newInstance = (SingletonHungry) ois.readObject(); System.out.println(instance); System.out.println(newInstance); System.out.println(instance == newInstance);

来看下结果图片

果然出现了两个不同的对象!这种反序列化攻击其实解决方式也简单,重写反序列化时要调用的 readObject 方法即可

private Object readResolve(){     return instance; }

这样在反序列化时候永远只读取 instance 这一个实例,保证了单例的实现。

真正安全的单例: 枚举方式

public enum SingleTonEnum {     /**      * 实例对象      */     INSTANCE;     public void doSomething() {         System.out.println("doSomething");     } }

调用方法

public class Main {     public static void main(String[] args) {         SingleTonEnum.INSTANCE.doSomething();     } }

枚举模式实现的单例才是真正的单例模式,是完美的实现方式

有人可能会提出疑问:枚举是不是也能通过反射来破坏其单例实现呢?

试试呗,修改枚举的测试类

class E{     public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {         Class singleTonEnumClass = SingleTonEnum.class;         Constructor declaredConstructor = singleTonEnumClass.getDeclaredConstructor();         declaredConstructor.setAccessible(true);         SingleTonEnum singleTonEnum = declaredConstructor.newInstance();         SingleTonEnum instance = SingleTonEnum.INSTANCE;         System.out.println("instance = " + instance);         System.out.println("singleTonEnum = " + singleTonEnum);     } }

结果

没有无参构造?我们使用 javap 工具来查下字节码看看有啥玄机

好家伙,发现一个有参构造器 String Int ,那就试试呗

//获取构造器的时候修改成这样子 Constructor declaredConstructor = singleTonEnumClass.getDeclaredConstructor(String.class,int.class);

结果

好家伙,抛出了异常,异常信息写着: 「Cannot reflectively create enum objects」

源码之下无秘密,我们来看看 newInstance() 到底做了什么?为啥用反射创建枚举会抛出这么个异常?

真相大白!如果是枚举,不允许通过反射来创建,这才是使用 enum 创建单例才可以说是真正安全的原因!

感谢各位的阅读,以上就是"如何使用单例模式"的内容了,经过本文的学习后,相信大家对如何使用单例模式这一问题有了更深刻的体会,具体使用情况还需要大家实践验证。这里是,小编将为大家推送更多相关知识点的文章,欢迎关注!

实例 对象 反射 代码 线程 序列 安全 方式 问题 攻击 写法 模式 时候 结果 内存 空间 构造器 保证 测试 两个 数据库的安全要保护哪些东西 数据库安全各自的含义是什么 生产安全数据库录入 数据库的安全性及管理 数据库安全策略包含哪些 海淀数据库安全审计系统 建立农村房屋安全信息数据库 易用的数据库客户端支持安全管理 连接数据库失败ssl安全错误 数据库的锁怎样保障安全 广东猪兼强互联网科技有限公司 科技互联网产品 科技赋能下的互联网金融 中国 科技 互联网 互联网与未来科技 天下捷融互联网科技 互联网金融和金融科技 青岛中康互联网医疗科技有限公司 银川新华互联网科技职业技术培训学校图片 港股科技和互联网 阿里互联网科技企业 卓翼科技产业互联网合作 科技赋能视角下互联网保险发展 长沙互联网科技 上海京穆互联网科技有限公司 怎样加入科技互联网 领航科技大厦互联网公司 上海互联网科技公司ppt 福仕德教育科技互联网 互联网科技创新方面的资料 北京路客互联网科技有限公司 互联网金融 金融科技 嘟嘟瓜互联网科技 互联网高科技企业 哈尔滨新华互联网科技 华中科技有哪些互联网大佬 哈尔滨新华互联网科技学院 民营互联网公司国防科技 保山浩联互联网科技有限公司 科技互联网专业务虚会材料
0