Java单例模式写法
创始人
2025-05-31 08:08:38
0

目录

  • 单例模式
    • 饿汉模式实现单例
    • 懒汉模式实现单例
      • 单线程版
      • 多线程版
      • 多线程版优化
  • 小结

单例模式

单例模式能保证某个类在程序中只存在唯一一份实例, 而不会创建出多个实例.
为什么要保证只存在一份对象呢?
因为有些对象管理的内存数据可能会很多, 可能有些项目里就一个对象运行起来就吃上百G的内存空间, 如果不小心多new了几个, 那系统可能直接崩溃了.

饿汉模式实现单例

类加载的同时, 创建实例.

class Singleton {private static Singleton singleton = new Singleton();private Singleton() {}    //不允许外部调用构造方法public static Singleton getSingleton() {return singleton;  //将创建好的实例返回}
}

这里只是单纯的读操作, 因此该模式是线程安全的.

懒汉模式实现单例

核心思想 :非必要不创建.
加载的时候不创建实例. 第一次使用的时候才创建实例.

单线程版

class Singleton {private static Singleton singleton = null;private Singleton() {}  //修改了构造方法的访问修饰权限符, 只有在类内部才能访问构造方法public static Singleton getSingleton() {if (singleton == null) {singleton = new Singleton();}return singleton;}
}

上面的懒汉模式实现是线程不安全的.
为什么不安全呢?
比如两个线程同时调用 getSingleton 方法时, 此时 singleton 还为空, t1 线程和 t2 线程都走进了判断语句, 判断通过, 它两都 new 出了对象, 这与我们预期的创建一个对象不符, 所以线程不安全.

我们可以通过加锁来解决这一问题, 下面是多线程版 , 线程安全.

多线程版

怎么加锁呢?
看看这样加锁是否可行:

class Singleton {private static Singleton singleton;private Singleton() {}public static Singleton getSingleton() {if(singleton == null) {synchronized (Singleton.class) {singleton = new Singleton();}}return singleton;}
}

显然这样是不行的, 当两个线程同时调用 getSingleton 方法时, 此时 singleton 还为空, t1 线程和 t2 线程都走进了判断语句, 判断通过, 然后通过竞争锁, 竞争成功的线程先 new 对象, 另一个线程后 new 对象, 这也是 new 了多个对象, 与预期不符.

我们再来看看给方法加锁:

class Singleton {private static Singleton singleton;private Singleton() {}public synchronized static Singleton getSingleton() {if(singleton == null) {singleton = new Singleton();}return singleton;}
}

当多个线程同时调用 getSingleton 方法时, 通过竞争锁, 竞争成功的线程先进去创建完对象出来后, 其他线程再来获取对象就不会再创建对象了.

其实这里还有一个问题, 那就是指令重排序问题, 什么意思呢?
举个例字 :
假设我们有两个线程 t1 和 t2,
t1 有个操作是: s = new Student();
t2 有个操作是: if(s != null) { s.learn(); }

这个操作 : s = new Student(); 大体可以分为三个步骤:

  1. 申请内存空间
  2. 调用构造方法(初始化内存的数据)
  3. 把对象的引用赋值给 s (内存地址的赋值)

如果是单线程, 此处进行指令重排序,步骤2和步骤3是可以调换顺序的, 重排序后可能就是132执行了, 这对单线程结果没影响, 但多线程就不行了.

回到上面的 t1 和 t2, 如果 t1 的操作进行指令重排序, 就会先申请内存, 然后把这个内存地址赋值给 s (注意这里还没有调用构造方法, 没有new对象), 这时 s 还是 null ,这个时候如果线程 t2 刚好进行 if 判断则会直接进入, 然后调用 s 的方法, 因为 s 为空, 就会抛出空指针异常.

这也是上面给方法加锁代码存在的问题, 如果得到的 singleton 为空就调用这里面的方法, 那就会产生空指针异常.

如何避免发生指令重排序呢?
我们可以加个 volatile 来禁止指令重排序, 在下面代码中体现.

多线程版优化

class Singleton {private volatile static Singleton singleton; //禁止对singleton进行指令重排序private Singleton() {}public static Singleton getSingleton() {//注意这里有两个 if 判断是否为空!!! if(singleton == null) {synchronized (Singleton.class) {if(singleton == null) {singleton = new Singleton();}}}return singleton;}
}

为什么要两个 if 判断呢?
加锁 / 解锁是一件开销比较高的事情. 而懒汉模式的线程不安全只是发生在首次创建实例的时候.因此后续使用的时候, 不必再进行加锁了.
这样可以让加锁操作只在第一次创建实例的时候出现.

小结

单例模式线程安全问题 :

  1. 饿汉模式, 天然就是安全的, 只是读操作.

  2. 懒汉模式, 不安全的, 有读也有写.

如何将懒汉模式变安全:

  1. 加锁, 把 if new 变为原子操作.
  2. 双重 if, 减少不必要的加锁操作.
  3. 使用 volatile 禁止指令重排序, 保证后续线程拿到的是完整对象.

相关内容

热门资讯

前端-session、jwt 目录:   (1)session (2&#x...
linux入门---制作进度条 了解缓冲区 我们首先来看看下面的操作: 我们首先创建了一个文件并在这个文件里面添加了...
关于测试,我发现了哪些新大陆 关于测试 平常也只是听说过一些关于测试的术语,但并没有使用过测试工具。偶然看到编程老师...
前缀和与对数器与二分法 1. 前缀和 假设有一个数组,我们想大量频繁的去访问L到R这个区间的和,...
nodejs:本地安装nvm实... 一、背景-使用不同版本node的原因 vue3+ts、nuxt3版本,node...
JAVA集合知识整理 Java集合知识整理 HashMap相关 HashMap的底层数据结构:jdk1.8之...
无刷直流电机介绍及单片机控制实... 无刷直流电机介绍及单片机控制实例前言基本概念优势与劣势使用寿命基本结构使用单片机控制实例电子调速器&...
fwdiary(2) dp2 1.传纸条  AcWing 275. 传纸条 - AcWing 走两条路,走一条最大的...
常用的DOS命令 常用的DOS命令 DOS(Disk Operating System,磁...
<C++> 类和对象(下) 1.const成员函数将const修饰的“成员函数”称之为const成员函数,cons...