Java 单例模式从入门到入坟(全解全析)
代码地址:https://github.com/gaohanghang/leetcode
一,什么是单例模式
二,介绍
这两个可以先不看,都是概念性的东西,直接看后面的就行,当然看了也可以 🐶
单例模式为什么那么常问?
是因为这个题目可以问到很多知识点。比如线程安全、类加载机制、synchronized 的原理、volatile 的原理、指令重排与内存屏障、枚举的实现、反射与单例模式、序列化如何破坏单例、CAS、CAS 的 ABA 问题、Threadlocal 等知识。一般情况下,只需要从单例开始问起,大概就可以完成一场面试的整个流程,把想问的东西都问完,可以比较全面的了解一个面试者的水平。——
Java架构师联盟
一,什么是单例模式
单例模式即一个 JVM 内存中只存在一个类的对象实例。
https://refactoringguru.cn/design-patterns/singleton
单例模式(Singleton Pattern)是 Java 中最简单的设计模式之一。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。
这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。
注意:
- 1、单例类只能有一个实例。
- 2、单例类必须自己创建自己的唯一实例。
- 3、单例类必须给所有其他对象提供这一实例。
二,介绍
意图:保证一个类仅有一个实例,并提供一个访问它的全局访问点。
主要解决:一个全局使用的类频繁地创建与销毁。
何时使用:当您想控制实例数目,节省系统资源的时候。
如何解决:判断系统是否已经有这个单例,如果有则返回,如果没有则创建。
关键代码:构造函数是私有的。
应用实例:
- 1、一个班级只有一个班主任。
- 2、Windows 是多进程多线程的,在操作一个文件的时候,就不可避免地出现多个进程或线程同时操作一个文件的现象,所以所有文件的处理必须通过唯一的实例来进行。
- 3、一些设备管理器常常设计为单例模式,比如一个电脑有两台打印机,在输出的时候就要处理不能两台打印机打印同一个文件。
优点:
- 1、在内存里只有一个实例,减少了内存的开销,尤其是频繁的创建和销毁实例(比如管理学院首页页面缓存)。
- 2、避免对资源的多重占用(比如写文件操作)。
缺点:没有接口,不能继承,与单一职责原则冲突,一个类应该只关心内部逻辑,而不关心外面怎么样来实例化。
使用场景:
- 1、要求生产唯一序列号。
- 2、WEB 中的计数器,不用每次刷新都在数据库里加一次,用单例先缓存起来。
- 3、创建的一个对象需要消耗的资源过多,比如 I/O 与数据库的连接等。
注意事项:getInstance() 方法中需要使用同步锁 synchronized (Singleton.class) 防止多线程同时进入造成 instance 被多次实例化。
三,实现
3.1 懒汉式,线程不安全(不推荐使用)
这种方式是最基本的实现方式,这种实现最大的问题就是不支持多线程。因为没有加锁 synchronized,所以严格意义上它并不算单例模式。这种方式 lazy loading 很明显,不要求线程安全,在多线程不能正常工作。
3.1.1 视频讲解
https://www.bilibili.com/video/BV1ff4y1X7v7
3.1.2 代码
/**
* @Description 懒汉式 , 线程不安全
* @Author Gao Hang Hang
* @Date 2019-09-10 21:07
**/
public class Singleton {
private static Singleton instance;
// 构造器私有,其他类就无法通过new Singleton() 来创建对象实例了
private Singleton() {
}
// 获取实例的方法
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
3.2 懒汉式,线程安全(不推荐使用)
这种方式具备很好的 lazy loading,能够在多线程中很好的工作,但是,效率很低,99% 情况下不需要同步。优点:第一次调用才初始化,避免内存浪费。缺点:必须加锁 synchronized 才能保证单例,但加锁会影响效率。getInstance() 的性能对应用程序不是很关键(该方法使用不太频繁)。
3.2.1 视频讲解
https://www.bilibili.com/video/BV17K4y1v7md
3.2.2 代码
/**
* @Description 懒汉式 , 线程安全
* @Author Gao Hang Hang
* @Date 2019-09-10 21:10
**/
public class Singleton {
private static Singleton instance;
private Singleton() {
}
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
3.3 饿汉式(推荐使用)
这种方式比较常用,但容易产生垃圾对象。优点:没有加锁,执行效率会提高。缺点:类加载时就初始化,浪费内存。它基于 classloader 机制避免了多线程的同步问题,不过,instance 在类装载时就实例化,虽然导致类装载的原因有很多种,在单例模式中大多数都是调用 getInstance 方法, 但是也不能确定有其他的方式(或者其他的静态方法)导致类装载,这时候初始化 instance 显然没有达到 lazy loading 的效果。
3.3.1 视频讲解
https://www.bilibili.com/video/BV1XD4y1m72M/
3.3.2 代码
/**
* @Description 饿汉式
* @Author Gao Hang Hang
* @Date 2019-09-10 21:12
**/
public class Singleton {
private static Singleton instance = new Singleton();
private Singleton() {}
public static Singleton getInstance() {
return instance;
}
}
3.4 饿汉 变种
表面上看起来差别挺大,其实更第三种方式差不多,都是在类初始化即实例化instance。
3.4.1 视频讲解
https://www.bilibili.com/video/BV1yi4y137vP/
3.4.2 代码
/**
* @Description 饿汉,变种
* 表面上看起来差别挺大,其实更第三种方式差不多,都是在类初始化即实例化instance。
* @Author Gao Hang Hang
* @Date 2019-09-10 21:14
**/
public class Singleton {
private static Singleton instance = null;
static {
instance = new Singleton();
}
private Singleton() {}
public static Singleton getInstance() {
return instance;
}
}
3.5 静态内部类(推荐使用)
这种方式能达到双检锁方式一样的功效,对静态域使用延迟初始化,但实现更简单。这种方式只适用于静态域的情况,双检锁方式可在实例域需要延迟初始化时使用。
3.5.1 视频讲解
https://www.bilibili.com/video/BV19A411Y7Mz
3.5.2 代码
/**
* @Description 静态内部类
* @Author Gao Hang Hang
* @Date 2019-09-10 21:16
**/
public class Singleton {
private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}
private Singleton() {
}
public static Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
}
3.6 枚举(推荐使用)
这种实现方式还没有被广泛采用,但这是实现单例模式的最佳方法。它更简洁,自动支持序列化机制,绝对防止多次实例化。这种方式是 Effective Java 作者 Josh Bloch 提倡的方式,它不仅能避免多线程同步问题,而且还自动支持序列化机制,防止反序列化重新创建新的对象,绝对防止多次实例化。不过,由于 JDK1.5 之后才加入 enum 特性,用这种方式写不免让人感觉生疏,在实际工作中,也很少用。
3.6.1 视频讲解
https://www.bilibili.com/video/BV1kK4y1v7HD
3.6.2 代码
/**
* @Description 枚举
* @Author Gao Hang Hang
* @Date 2019-09-10 21:18
**/
public enum Singleton {
INSTANCE;
public void whateverMethod() {
System.out.println("哈哈");
}
}
测试类 Test.java
public class Test {
public static void main(String[] args) throws IOException, ClassNotFoundException {
Singleton.INSTANCE.whateverMethod();
// 简单引用
Singleton instance0 = Singleton.INSTANCE;
Singleton instance1 = Singleton.INSTANCE;
System.out.println("instance0===" + instance0.hashCode());
System.out.println("instance1===" + instance1.hashCode());
// 反射测试
Class clazz = Singleton.class;
Singleton instance2 = (Singleton) Enum.valueOf(clazz, "INSTANCE");
Singleton instance3 = (Singleton) Enum.valueOf(clazz, "INSTANCE");
System.out.println("instance2===" + instance2.hashCode());
System.out.println("instance3===" + instance3.hashCode());
// 序列化测试
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(new File("test")));
oos.writeObject(instance0);
oos.close();
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(new File("test")));
Singleton instance4 = (Singleton) ois.readObject();
ois.close();
ObjectInputStream ois1 = new ObjectInputStream(new FileInputStream(new File("test")));
Singleton instance5 = (Singleton) ois1.readObject();
ois1.close();
System.out.println("instance4===" + instance4.hashCode());
System.out.println("instance5===" + instance5.hashCode());
}
}
运行结果:
哈哈
instance0===1927950199
instance1===1927950199
instance2===1927950199
instance3===1927950199
instance4===1927950199
instance5===1927950199
3.7 双重检验锁(推荐使用)
这种方式采用双锁机制,安全且在多线程情况下能保持高性能。getInstance() 的性能对应用程序很关键。
3.7.1 视频讲解
https://www.bilibili.com/video/BV1f5411a7sh/
3.7.2 代码
package a0算法面试题.单例设计模式.单例模式的七种写法.a7;
/**
* @Description 双重校验锁
* @Author Gao Hang Hang
* @Date 2019-09-10 21:19
**/
public class Singleton {
/*
volatile 修饰,
singleton = new Singleton() 可以拆解为3步:
1、分配对象内存(给singleton分配内存)
2、调用构造器方法,执行初始化(调用 Singleton 的构造函数来初始化成员变量)。
3、将对象引用赋值给变量(执行完这步 singleton 就为非 null 了)。
若发生重排序,假设 A 线程执行了 1 和 3 ,还没有执行 2,B 线程来到判断 NULL,B 线程就会直接返回还没初始化的 instance 了。
volatile 可以避免重排序。
*/
private volatile static Singleton singleton;
private Singleton() {
}
public static Singleton getSingleton() {
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
}
四,单例模式的实际例子
内容来自《设计模式(Java版)》
任务描述:在整个项目中需要一个共享访问点或共享数据,例如,一个Web页面上的计数器,可以不用把每次刷新都记录到数据库中,使用单例模式保持计数器的值,并确保是线程安全的。
下述代码用于实现该任务描述,使用单例模式记录访问次数。
首先编写一个单例模式类 GlobalNum,其代码如下所示。
GlobalNum.java
**
**
public class GlobalNum {
private static GlobalNum gn = new GlobalNum();
private int num = 0;
public static GlobalNum getInstance() {
return gn;
}
public synchronized int getNum() {
return ++num;
}
}
上述代码中创建一个饿汉式单例类GlobalNum,其中getNum()方法用于返回访问次数,并且使用synchronized对该方法进行线程同步。
编写一个测试代码,用于访问GlobalNum单例类,其代码如下所示。
SingleDemo.java
**
**
public class SingleDemo {
// 测试单例模式
public static void main(String[] args) {
// 创建线程A
NumThread threadA = new NumThread("线程A");
// 创建线程B
NumThread threadB = new NumThread("线程B");
// 启动线程
threadA.start();
threadB.start();
}
}
// 线程类
class NumThread extends Thread{
private String threadName;
public NumThread(String name) {
threadName = name;
}
// 重新线程的 run 方法(线程任务)
@Override
public void run() {
GlobalNum gnObj = GlobalNum.getInstance();
for (int i = 0; i < 5; i++) {
System.out.println(threadName + "第" + gnObj.getNum() + "次访问!");
try {
this.sleep(1000); // 线程休眠1000毫秒
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
上述代码在主程序中创建两个子线程,通过这两个子线程演示对单例模式下唯一实例的访问。因为 GlobalNum 的对象是单例的,所以能够统一地对线程访问次数进行统计。由于上述代码是多线程的,运行结果每次都有可能出现不同,可能的运行结果示意如下。
线程B第2次访问!
线程A第1次访问!
线程A第3次访问!
线程B第4次访问!
线程A第6次访问!
线程B第5次访问!
线程A第7次访问!
线程B第8次访问!
线程A第10次访问!
线程B第9次访问!
五,问题
5.1 问题1: 为什么构造函数要使用 private
构造器私有,其他类就无法通过 new Singleton() 来创建对象实例
/**
* @Description 懒汉式 , 线程不安全
* @Author Gao Hang Hang
* @Date 2019-09-10 21:07
**/
public class Singleton {
private static Singleton instance;
// 构造器私有,其他类就无法通过new Singleton() 来创建对象实例
private Singleton() {
}
// 获取实例的方法
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
5.1.1 视频讲解
https://www.bilibili.com/video/BV1X54y1S7F1
5.2 问题2:双重校验锁—为什么使用 volatile 和两次判空校验
1 为什么要进行两次非空校验?
第一个 if 判断是为了减少性能开销
第二个 if 判断是为了避免生成多个对象实例。
2 为什么要用 volatile 关键字?
为了禁止 JVM 的指令重排,指令重排会导致对象未初始化的情况,造成报错
5.2.1 视频讲解
视频地址:https://www.bilibili.com/video/BV1dA411a7qB
说的比较慢,声音小也比较小,可以点去blibli观看,选2倍速播放,再把音量🔊调大点
https://www.bilibili.com/video/BV1dA411a7qB
原文地址:双重校验锁 --使用volatile和两次判空校验
5.3 问题3:单例模式中唯一实例为什么要用静态?
5.3.1 视频讲解
https://www.bilibili.com/video/BV1TK4y1v71k/
因为 getInstance() 是静态方法,而静态方法不能访问非静态成员变量,所以 instanc 必须是静态成员变量
public static Singleton getInstance(){
}
那么为什么 getInstance() 是静态方法?
因为构造器是私有的,程序调用类中方法只有两种方式,
① 创建类的一个对象,用该对象去调用类中方法;
② 使用类名直接调用类中方法,格式“类名.方法名()”;
Singleton instance = Singleton.getInstance();
构造函数私有化后第一种情况就不能用,只能使用第二种方法。
// 构造器私有,其他类就无法通过 new Singleton() 来创建对象实例了
private Singleton() {
}
为什么要私有化构造器呢?
目的是禁止其他程序创建该类的对象
如果构造函数不是私有的,每个人都可以通过 new Singleton() 创建类的实例,因此不再是单例。根据定义,对于一个单例,只能存在一个实例。
5.4 问题4:单例模式中成员变量为什么一定要是私有的private
public class Singleton {
public static Singleton singleton = new Singleton();
private Singleton() {
}
public static void main(String[] args) {
Singleton.singleton = null;
System.out.println(Singleton.singleton);
}
}
运行结果为null;
上面可以看做是一个单例模式,下面是调用该类并将单例的成员变量改成null。
万一有程序员这么做了,后面的程序员再用这个类时就是空,所以为了安全不要这么写
5.5 问题5:饿汉式和懒汉式的区别?
懒汉式:先天性线程不安全,当真正需要该实例的时候才去加载,需要我们自己人为上锁控制线程安全问题。
饿汉式:先天性线程安全,当我们项目在启动的时候创建该实例,会导致项目启动比较慢
5.6 问题6:静态内部类与双重校验锁的区别?
静态内部类使用静态关键字去保证我们实例是单例的。
而我们的双重校验锁采用 lock 锁保证安全的。
5.7 问题7:为什么静态内部类写法中,静态类里面获取单例对象要用 final 修饰?
用 final 更多的意义在于提供语法约束。毕竟你是单例,就只有这一个实例,不可能再指向另一个实例。instance有了 final 的约束,后面再有人不小心编写了修改其指向的代码就会报语法错误。
这就好比 @Override 注解,你能保证写对方法名和参数,那不写注解也没问题,但是有了注解的约束,编译器就会帮你检查,还能防止别人乱改—— 公众号《Java课代表》作者
5.8 问题8:单例饿汉式为什么没有线程安全性问题?
在 getInstance() 获取实例的方法中,没有对资源进行非原子性操作,instance 在类加载过程中就实例化了
从Java的并发编程:线程的安全性问题的分析 这篇文章,我们知道,线程的安全性问题要满足下面三个条件:
- 多线程环境下
- 多个线程共享一个资源
- 对资源进行非原子性操作
而对于单例饿汉式确不满足第三个条件,我们可以用下面的Java程序示例来看一下(实现是创建一个饿汉式的类,再用20个线程去调用):
public class Singleton {
// 私有化构造方法
private Singleton () {}
private static Singleton instance = new Singleton();
public static Singleton getInstance() {
return instance;
}
// 多线程的环境下
// 必须有共享资源
// 对资源进行非原子性操作
}
在 getInstance() 获取实例的方法中,没有对资源进行非原子性操作,我们所获取的对象都是一样的,可以用一个MultiThreadMain.java 方法来看一下,创建20个线程去调用:
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class MultiThreadMain {
public static void main(String[] args) {
ExecutorService threadPool = Executors.newFixedThreadPool(20);
for (int i = 0; i < 20; i++) {
threadPool.execute(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + ":" +Singleton.getInstance());
}
});
}
threadPool.shutdown();
}
}
结果是:
pool-1-thread-14:a0算法面试题.单例设计模式.单例模式的七种写法.a3.Singleton@342c15c8
pool-1-thread-15:a0算法面试题.单例设计模式.单例模式的七种写法.a3.Singleton@342c15c8
pool-1-thread-20:a0算法面试题.单例设计模式.单例模式的七种写法.a3.Singleton@342c15c8
pool-1-thread-16:a0算法面试题.单例设计模式.单例模式的七种写法.a3.Singleton@342c15c8
pool-1-thread-18:a0算法面试题.单例设计模式.单例模式的七种写法.a3.Singleton@342c15c8
pool-1-thread-8:a0算法面试题.单例设计模式.单例模式的七种写法.a3.Singleton@342c15c8
pool-1-thread-11:a0算法面试题.单例设计模式.单例模式的七种写法.a3.Singleton@342c15c8
pool-1-thread-2:a0算法面试题.单例设计模式.单例模式的七种写法.a3.Singleton@342c15c8
pool-1-thread-4:a0算法面试题.单例设计模式.单例模式的七种写法.a3.Singleton@342c15c8
pool-1-thread-6:a0算法面试题.单例设计模式.单例模式的七种写法.a3.Singleton@342c15c8
pool-1-thread-1:a0算法面试题.单例设计模式.单例模式的七种写法.a3.Singleton@342c15c8
pool-1-thread-5:a0算法面试题.单例设计模式.单例模式的七种写法.a3.Singleton@342c15c8
pool-1-thread-9:a0算法面试题.单例设计模式.单例模式的七种写法.a3.Singleton@342c15c8
pool-1-thread-13:a0算法面试题.单例设计模式.单例模式的七种写法.a3.Singleton@342c15c8
pool-1-thread-12:a0算法面试题.单例设计模式.单例模式的七种写法.a3.Singleton@342c15c8
pool-1-thread-19:a0算法面试题.单例设计模式.单例模式的七种写法.a3.Singleton@342c15c8
pool-1-thread-7:a0算法面试题.单例设计模式.单例模式的七种写法.a3.Singleton@342c15c8
pool-1-thread-3:a0算法面试题.单例设计模式.单例模式的七种写法.a3.Singleton@342c15c8
pool-1-thread-10:a0算法面试题.单例设计模式.单例模式的七种写法.a3.Singleton@342c15c8
pool-1-thread-17:a0算法面试题.单例设计模式.单例模式的七种写法.a3.Singleton@342c15c8
可以看到,对于饿汉式的单例模式,是没有线程安全性问题的。但是饿汉式会造成对资源的浪费,比如说我没有调用这个 Singleton 类的时候,它已经创建好给我们了。
类加载过程的线程安全性保证
饿汉、静态内部类、枚举均是通过定义静态的成员变量,以保证单例对象可以在类初始化的过程中被实例化。
这其实是利用了 ClassLoader 的线程安全机制。ClassLoader 的 loadClass 方法在加载类的时候使用了 synchronized 关键字。
所以, 除非被重写,这个方法默认在整个装载过程中都是线程安全的。所以在类加载过程中对象的创建也是线程安全的。
枚举其实底层是依赖 Enum 类实现的,这个类的成员变量都是 static 类型的,并且在静态代码块中实例化的,和饿汉有点像, 所以他天然是线程安全的,所以,枚举其实也是借助了synchronized的
5.9 问题9:以下哪种方式实现的单例是线程安全的
A. 枚举
B. 静态内部类
C. 双检锁模式
D. 饿汉式
正确答案:A B C D
第一种:饿汉模式(线程安全)
第二种:懒汉模式 (如果方法没有synchronized,则线程不安全)
第三种:懒汉模式改良版(线程安全,使用了double-check,即check-加锁-check,目的是为了减少同步的开销)
第四种:利用私有的内部工厂类(线程安全,内部类也可以换成内部接口,不过工厂类变量的作用域要改为public了。)
5.10 问题10:怎么不使用 synchronized 和 lock 实现一个线程安全的单例吗?
以上实现主要用到了两点来保证单例,一是JVM的类加载机制,另一个就是加锁了。那么有没有不加锁的线程安全的单例实现吗?
5.10.1 CAS实现单例
5.10.1.1 什么是 CAS?
CAS 是一项乐观锁技术,当多个线程尝试使用 CAS 同时更新一个变量时,只有其中一个线程能更新成功,而其它线程都失败,失败的线程并不会被挂起,而是被告知这次竞争中失败,并可以再次尝试。
5.10.1.2 代码实现
CAS 实现单例:
public class Singleton {
// AtomicReference 提供了可以原子的读写对象引用的一种机制
private static final AtomicReference<Singleton> INSTANCE = new AtomicReference<Singleton>();
// 私有化构造器
private Singleton() {
}
// 获取实例的 getInstance() 方法
public static Singleton getInstance() {
for(;;) {
// 从 INSTANCE中 获取实例
Singleton singleton = INSTANCE.get();
// 如果实例不为空就返回
if (null != singleton) {
return singleton;
}
// 实例为空就创建实例
singleton = new Singleton();
// compareAndSet() 主要的作用是通过比对两个对象,然后更新为新的对象
if (INSTANCE.compareAndSet(null, singleton)) {
return singleton;
}
}
}
}
以下为测试结果:可以看出都是相同的实例
5.10.1.3 使用 CAS 实现的单例有没有什么优缺点呀?
优点:
用 CAS 的好处在于不需要使用传统的锁机制来保证线程安全,CAS 是一种基于忙等待的算法,依赖底层硬件的实现,相对于锁它没有线程切换和阻塞的额外消耗,可以支持较大的并行度。
缺点:
CAS的一个重要缺点在于如果忙等待一直执行不成功(一直在死循环中),会对 CPU 造成较大的执行开销。
另外,代码中,如果 N 个线程同时执行到 singleton = new Singleton(); 的时候,会有大量对象被创建,可能导致内存溢出。
5.10.2 使用 ThreadLocal 实现“单例”模式
标题中单例之所以带着双引号,是因为并不能保证整个应用全局唯一,但是可以保证线程唯一。
5.10.2.1 ThreadLocal是什么?
ThreadLocal 会为每一个线程提供一个独立的变量副本,从而隔离了多个线程对数据的访问冲突。对于多线程资源共享的问题,同步机制( synchronized )采用了“以时间换空间”的方式,而 ThreadLocal 采用了“以空间换时间”的方式。
同步机制仅提供一份变量,让不同的线程排队访问,而 ThreadLocal 为每一个线程都提供了一份变量,因此可以同时访问而互不影响。
5.10.2.2 代码实现
使用 ThreadLocal 实现单例模式:
public class Singleton {
private static final ThreadLocal<Singleton> singleton = new ThreadLocal<Singleton>() {
@Override
protected Singleton initialValue() {
return new Singleton();
}
};
// 私有化构造器
private Singleton(){
}
// 获取实例的方法
public static Singleton getInstance() {
return singleton.get();
}
}
测试类:
public class Test {
public static void main(String[] args) {
System.out.println("main thread "+Singleton.getInstance());
System.out.println("main thread "+Singleton.getInstance());
System.out.println("main thread "+Singleton.getInstance());
Thread thread0 = new Thread(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + ":" + Singleton.getInstance());
}
});
Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + ":" + Singleton.getInstance());
}
});
thread0.start();
thread1.start();
}
}
结果:
**
**
两个线程(线程0和线程1)拿到的对象并不是同一个对象,但是同一线程能保证拿到的是同一个对象,即线程单例。
ThreadLocal 这种写法主要是考察面试者对于 ThreadLocal 的理解,以及是否可以把知识活学活用,但是实际上,这种所谓的"单例",其实失去了单例的意义..
六,破坏单例模式的方式
- 反射
- 序列和反序列化
这里我以静态内部类单例来举例,先看下静态内部类单例的代码
public class Singleton {
// 静态内部类
private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}
// 私有的构造方法
private Singleton() {
}
// 公有的获取实例方法
public static Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
}
6.1 反射破坏单例模式
我们来看代码
public class Test {
public static void main(String[] args) {
try {
// 很无聊的情况下,进行破坏
Class<?> clazz = Singleton.class;
// 通过反射拿到私有的构造方法
Constructor c = clazz.getDeclaredConstructor();
// 因为要访问私有的构造方法,这里要设为true,相当于让你有权限去操作
c.setAccessible(true);
// 暴力初始化
Object o1 = c.newInstance();
// 调用了两次构造方法,相当于 new 了两次
Object o2 = c.newInstance();
// 这里输出结果为false
System.out.println(o1 == o2);
} catch (Exception e) {
e.printStackTrace();
}
}
}
输出为false,说明内存地址不同,就是实例化了多次,破坏了单例模式的特性。
6.2 防止反射破坏单例模式
通过上面反射破坏单例模式的代码,我们可以知道,反射也是通过调用构造方法来实例化对象,那么我们可以在构造函数里面做点事情来防止反射,我们把静态内部类单例的代码改造一下,看代码
public class Singleton {
// 静态内部类
private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}
// 私有的构造方法
private Singleton() {
// 防止反射创建多个对象
if(SingletonHolder.INSTANCE != null){
throw new RuntimeException("不允许创建多个实例");
}
}
// 公有的获取实例方法
public static Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
}
这样我们在通过反射创建单例对象的时候,多次创建就会抛出异常
6.3 序列化破坏单例模式
用序列化的方式,需要在静态内部类(Singleton) 实现 Serializable 接口,代码在下面的防止序列化破坏单例模式里面
这里我们先来看下序列和反序列的代码
public class Test {
public static void main(String[] args) {
Singleton s1 = null;
//通过类本身获得实例对象
Singleton s2 = Singleton.getInstance();
FileOutputStream fos = null;
try {
// 序列化到文件中
fos = new FileOutputStream("SeriableSingleton.obj");
ObjectOutputStream oos = new ObjectOutputStream(fos);
oos.writeObject(s2);
oos.flush();
oos.close();
// 从文件中反序列化为对象
FileInputStream fis = new FileInputStream("SeriableSingleton.obj");
ObjectInputStream ois = new ObjectInputStream(fis);
s1 = (Singleton) ois.readObject();
ois.close();
// 对比结果,这里输出的结果为false
System.out.println(s1 == s2);
} catch (Exception e) {
e.printStackTrace();
}
}
}
结果为 false,说明也破坏了单例模式
6.4 防止序列化破坏单例模式
这里我们先来看下改造后的代码,然后分析原理
public class Singleton implements Serializable {
private static final long serialVersionUID = -4264591697494981165L;
// 静态内部类
private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}
// 私有的构造方法
private Singleton() {
// 防止反射创建多个对象
if(SingletonHolder.INSTANCE != null){
throw new RuntimeException("不允许创建多个实例");
}
}
// 公有的获取实例方法
public static Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
// 防止序列化创建多个对象,这个方法是关键
private Object readResolve(){
return SingletonHolder.INSTANCE;
}
}
在执行上面序列和反序列化代码,输出 true,是不是一脸懵逼,为什么加了一个 readResolve 方法,就能防止序列化破坏单例模式,下面就带着大家来看下序列化的源码:
public final Object readObject()throws IOException, ClassNotFoundException{
if (enableOverride) {
return readObjectOverride();
}
// if nested read, passHandle contains handle of enclosing object
int outerHandle = passHandle;
try {
// 看这里,看这里,就是我readObject0
Object obj = readObject0(false);
handles.markDependency(outerHandle, passHandle);
ClassNotFoundException ex = handles.lookupException(passHandle);
if (ex != null) {
throw ex;
}
if (depth == 0) {
vlist.doCallbacks();
}
return obj;
} finally {
passHandle = outerHandle;
if (closed && depth == 0) {
clear();
}
}
}
然后我们看下 readObject0 这个方法
private Object readObject0(boolean unshared) throws IOException {
...
//主要是这个判断
case TC_OBJECT:
//然后进入readOrdinaryObject这个方法
return checkResolve(readOrdinaryObject(unshared));
...
}
然后我们看下readOrdinaryObject 这个方法
private Object readOrdinaryObject(boolean unshared)throws IOException{
...
Object obj;
try {
// 这里判断是否有无参的构造函数,有的话就调用newInstance()实例化对象
obj = desc.isInstantiable() ? desc.newInstance() : null;
...
if (obj != null &&
handles.lookupException(passHandle) == null &&
desc.hasReadResolveMethod())
{
Object rep = desc.invokeReadResolve(obj);
...
}
这里的关键是 desc.hasReadResolveMethod() ,这段代码的意思是查看你的单例类里面有没有 readResolve 方法,有的话就利用反射的方式执行这个方法,具体是 desc.invokeReadResolve(obj) 这段代码,返回单例对象。这里其实是实例化了两次,只不过新创建的对象没有被返回而已。如果创建对象的动作发生频率增大,就意味着内存分配开销也就随之增大,这也算是一个缺点吧
七,枚举实现单例的原理
这种方式是 Effective Java 作者 Josh Bloch 提倡的方式,他认为单元素的枚举类型被作者认为是实现 Singleton 的最佳方法。这种方式不仅能避免多线程同步问题,而且还自动支持序列化机制,防止反序列化和反射攻击重新创建新的对象,绝对防止多次实例化。
/**
* 单例模式的枚举方式实现
**/
public enum Singleton {
INSTANCE;
public void whateverMethod() {
//do what you want
}
}
这种方式的原理是什么呢?趁这个机会在这里好好梳理一下枚举的概念。
枚举是 JDK5 中提供的一种语法糖,所谓语法糖就是在计算机语言中添加的某种语法,这种语法对语言的功能并没有影响,但是但是更方便程序员使用。只是在编译器上做了手脚,却没有提供对应的指令集来处理它。
其实 Enum 就是一个普通的类,它继承自 java.lang.Enum 类,这个可以通过反编译枚举类的字节码来理解。
使用 javac Singleton.java
得到字节码文件 Singleton.class 使用 javap Singleton.class
反解析字节码文件可以得到下面的内容:
public final class Singleton extends java.lang.Enum<Singleton> {
public static final Singleton INSTANCE;
public static Singleton[] values();
public static Singleton valueOf(java.lang.String);
public void whateverMethod();
static {};
}
枚举其实底层是依赖Enum类实现的,这个类的成员变量都是 static 类型的,并且在静态代码块中实例化的,和饿汉有点像, 所以他天然是线程安全的。
javap 是 jdk 自带的反解析工具。它的作用就是根据 class 字节码文件,反解析出当前类对应的 code 区(汇编指令)、本地变量表、异常表和代码行偏移量映射表、常量池等等信息。
由反编译后的代码可知,INSTANCE 被声明为 static 的,虚拟机会保证一个类的 <clinit>()
方法在多线程环境中被正确的加锁、同步。所以,枚举实现在实例化时是线程安全。
另外 Java 规范中规定,每一个枚举类型及其定义的枚举变量在 JVM 中都是唯一的,因此在枚举类型的序列化和反序列化上,Java 做了特殊的规定。在序列化的时候 Java 仅仅是将枚举对象的 name 属性输出到结果中,反序列化的时候则是通过 java.lang.Enum 的 valueOf() 方法来根据名字查找枚举对象,因此反序列化后的实例也会和之前被序列化的对象实例相同。
八,总结
不建议使用懒汉式,简单的可以使用饿汉式。涉及到反序列化创建对象时可以使用枚举方式。如果考虑到延迟加载 的话,可以采用静态内部类 Holder 的模式。如果对业务需求有特殊要求的时候可以采用双检查锁的单例。
九,Spring 单例 bean 与单例模式的区别
Spring 的单例是相对于容器的,而平常说的单例是相对于JVM的。
一个 JVM 可以有多个 Spring 容器,而且 Spring 中的单例也只是按 bean 的 id 来区分的
Spring 单例 bean 与单例模式的区别在于它们关联的环境不一样,单例模式是指在一个JVM 进程中仅有一个实例,而 Spring 单例是指一个 Spring bean 容器 (ApplicationContext) 中相同 id 的 bean 仅有一个实例。
首先看单例模式,在一个 JVM 进程中(理论上,一个运行的 JAVA 程序就必定有自己一个独立的 JVM)仅有一个实例,于是无论在程序中的何处获取实例,始终都返回同一个对象,以 Java 内置的 Runtime 为例(现在枚举是单例模式的最佳实践),无论何时何处获取,下面的判断始终为真:
// 基于懒汉模式实现
// 在一个JVM实例中始终只有一个实例
Runtime.getRuntime() == Runtime.getRuntime()
与此相比,Spring的单例 bean 是与其容器(ApplicationContext)密切相关的,所以在一个JVM进程中,如果有多个 Spring 容器,即使是单例 bean,也一定会创建多个实例,代码示例如下:
// 第一个Spring Bean容器
ApplicationContext context_1 = new FileSystemXmlApplicationContext("classpath:/ApplicationContext.xml");
Person yiifaa_1 = context_1.getBean("yiifaa", Person.class);
// 第二个Spring Bean容器
ApplicationContext context_2 = new FileSystemXmlApplicationContext("classpath:/ApplicationContext.xml");
Person yiifaa_2 = context_2.getBean("yiifaa", Person.class);
// 这里绝对不会相等,因为创建了多个实例
System.out.println(yiifaa_1 == yiifaa_2);
以下是 Spring 的配置文件:
<!-- 即使声明了为单例,只要有多个容器,也一定会创建多个实例 -->
<bean id="yiifaa" class="com.stixu.anno.Person" scope="singleton">
<constructor-arg name="username">
<value>yiifaa</value>
</constructor-arg>
</bean>
Spring 中的单例是按 bean 的 id 来区分的
在容器中每个 bean id 对应一个 bean。让我们举个例子来理解它。我们有一个 bean 类 Sample。我在 bean 定义中以此类定义了两个 bean,例如:
<bean id="id1" class="com.example.Sample" scope="singleton">
<property name="name" value="James Bond 001"/>
</bean>
<bean id="id7" class="com.example.Sample" scope="singleton">
<property name="name" value="James Bond 007"/>
</bean>
因此,当我尝试获取 ID 为“id1”的 bean 时,spring 容器将创建一个 bean,对其进行缓存并在 id1 引用过的地方返回相同的 bean。如果我尝试使用 id7 来获取它,那么将从 Sample 类创建另一个 bean,每次你使用 id7 引用该bean 时,都将对其进行缓存并返回。
这在单例模式中是不可能发生的。在单例模式中,总是为每个类创建一个对象。但是在 Spring 中,将范围设为Singleton 并不会限制容器从该类创建许多实例。它只是限制了相同 ID 的新对象的再次创建,当一个对象被请求使用相同 ID 时返回了先前创建的对象。
更多内容可以看这篇文章 An Interview Question on Spring Singletons
十,参考资料
https://refactoringguru.cn/design-patterns/singleton
Implementing Singleton with an Enum (in Java)
[枚举实现单例的原理](https://junzhou2016.github.io/2018/08/04/枚举实现单例原理 /)
面试处处都是坑啊?让实现线程安全的单例,又不让用synchronized
Why are enum singleton serialization safe?
Java并发编程——AtomicReference,解决并发修改多个属性
An Interview Question on Spring Singletons
Singleton design pattern vs Singleton beans in Spring container
十一,最后
欢迎关注我的公众号《骇客与画家》