单例模式的介绍、使用场景、七种写法及测试
概述
单例模式就是:确保一个类只有一个实例,并提供该实例的全局访问点。
使用场景
- 平常写代码的时候全局属性的保存(有状态的工具类对象,如使用 excel 导出带下拉的时候,需要一个 handler 缓存下拉值,这个下拉值需要全局使用)
- 多个模块使用同一个数据源连接对象
- 多线程的线程池也是被设计为单例,方便对池中现成进行控制
适用场景
- 需要频繁实例化然后销毁的对象
- 创建对象耗时过多或耗资源过多,但是又会经常用到的对象
- 方便资源相互通信的环境
实现方式
是否必须用单例?
在不需要维持任何状态下,仅仅用于全局访问,这个使用使用静态类的方式更加方便;但如果需要被继承以及需要维持一些特定状态的情况下,就适合使用单例模式,以下这种方式在我们开发中非常常见。
1 | public class Singleton00 { |
单例的七种实现方式
并发环境测试代码
因为涉及到判断我们使用的单例模式是否是线程安全的,所以我们需要一个并发环境来测试,如下是具体代码,整体思路是,设置一个计数器,然后启动一个线程,计数器减一,当计数器为0时,所有线程同时启动,通过 getInstance 方法拿对象
1 | public class Main { |
懒汉式(线程不安全)
优点:延迟实例化,用到对象的时候再new
缺点:多线程环境下,线程不安全,多个线程同时进入 getInstance 方法,并且都判断自己为 null ,所以就会出现多次 new Singleton01() 的情况 【不推荐】
1 | public class Singleton01 { |
并发环境下获取实例测试,会打印多次,也就是会构造多个对象
1 | Singleton01实例化 |
懒汉式(线程安全)
既然上述 getInstance() 的方法不安全,那就加个锁好了,别让线程都进来,没抢到的等着就行
但是,每次访问 getInstance() 都需要锁占用导致资源浪费 【不推荐】
1 | public class Singleton02 { |
并发测试:
1 | Singleton02实例化 |
饿汉式(线程安全)
上述线程不安全的问题主要是由于实例化了多次,我们这次类加载的时候就直接实例化,从而避免了实例化多次的情况发生,但是也不能节约资源了【不推荐】
1 | public class Singleton03 { |
使用类的内部类(线程安全)
因为用的时候才会加载内部类,jvm 可以保证多线程下类的<clinit>
只会执行一次,其他线程都会阻塞等待
既实现了延迟加载,又实现了线程安全【推荐使用】
1 | public class Singleton04 { |
双重校验锁(线程安全)
双重锁的方式是方法级锁的优化,减少了部分获取实例的耗时。也满足了懒加载
这里为什么采用 volatile 关键字修饰实例对象?
因为instance = new Singleton05();
这段代码其实分三步执行:
- 为 instance 分配内存空间
- 初始化 instance
- 将 instance 指向分配的内存地址
由于 JVM 具有指令重排的特性,执行顺序也有可能变为1 -> 3 -> 2。指令重排在单线程环境下不会出现问题,但是在多线程环境下,会出现一种情况,一个线程 A 执行了 1 和 3,也就是分配了内存空间,还把这个对象指向了那个内存空间了,但是就是还没初始化呢,另一个线程 B 进入 getInstance 方法,一看对象不是空的,就直接返回了,但是此时返回这个是没有初始化的对象。
volatile 关键字可以禁止 JVM 的指令重排功能,保证多线程环境下也可以正常运行。
1 | public class Singleton05 { |
CAS「AtomicReference」(线程安全)
java 并发库提供了很多原子类来支持并发访问的数据安全性;AtomicInteger
、AtomicBoolean
、AtomicLong
、AtomicReference
;AtomicReference<V>
可以封装引用一个V实例,支持并发访问。
使用CAS的好处就是不需要使用传统的加锁方式保证线程安全,而是依赖于CAS的忙等算法,依赖于底层硬件的实现,来保证线程安全。相对于其他锁的实现没有线程的切换和阻塞也就没有了额外的开销,并且可以支持较大的并发性。
当然CAS也有一个缺点就是忙等,如果一直没有获取到将会处于死循环中。
1 | public class Singleton06 { |
枚举(线程安全)
枚举有两种方式,一种是新建一个类,就是来做单例的;
还有一种是对已有类改造为单例模式的场景。
枚举实现单例好处:这种方式解决了最主要的;线程安全、自由串行化、单一实例。
可以防止反射攻击
新建一个类做单例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15public enum Singleton071 {
/** 实例 */
INSTANCE;
public void businessMethod() {
System.out.println("业务方法~");
}
public static void main(String[] args) {
Singleton071 instance = Singleton071.INSTANCE;
Singleton071 instance1 = Singleton071.INSTANCE;
// true
System.out.println(instance1 == instance);
}
}对已有类改造为单例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25public class Singleton072 {
private Singleton072() {
System.out.println("Singleton072实例化");
}
private enum SingletonEnum {
/** 枚举对象 */
INSTANCE;
private final Singleton072 instance;
SingletonEnum() {
instance = new Singleton072();
}
private Singleton072 getInstance() {
return instance;
}
}
public static Singleton072 getInstance() {
return SingletonEnum.INSTANCE.getInstance();
}
}