本文已参与「新人创作礼」活动,一起开启掘金创作之路。
单例模式是最简单也是最基础的设计模式之一,下边一起学习一下单例模式!
一.单例模式的定义:
单例模式确保某个类只有一个实例,而且自行实例化并向整个系统提供这个实例。在计算机系统中,线程池、缓存、日志对象、对话框、打印机、显卡的驱动程序对象常被设计成单例。这些应用都或多或少具有资源管理器的功能。每台计算机可以有若干个打印机,但只能有一个Printer Spooler,以避免两个打印作业同时输出到打印机中。每台计算机可以有若干通信端口,系统应当集中管理这些通信端口,以避免一个通信端口同时被两个请求同时调用。总之,选择单例模式就是为了避免不一致状态,避免政出多头。
二.单例模式的特点
- 单例类只能有一个实例。
- 单例类必须自己创建自己的唯一实例。
- 单例类必须给所有其他对象提供这一实例。
- 单例模式保证了全局对象的唯一性,比如系统启动读取配置文件就需要单例保证配置的一致性。
三.线程安全问题:
一方面在获取单例的时候,要保证不能产生多个实例对象,后面会详细讲到五种实现方式;另一方面,在使用单例对象的时候,要注意单例对象内的实例变量是会被多线程共享的,推荐使用无状态的对象,不会因为多个线程的交替调度而破坏自身状态导致线程安全问题,比如我们常用的VO,DTO等(局部变量是在用户栈中的,而且用户栈本身就是线程私有的内存区域,所以不存在线程安全问题)。
四.实现单例模式的八种方式:
1.饿汉式(静态常量)【可用】
优点:这种写法比较简单,就是在类装载的时候就完成实例化。避免了线程同步问题。 缺点:在类装载的时候就完成实例化,没有达到Lazy Loading的效果。如果从始至终从未使用过这个实例,则会造成内存的浪费。
public class Singleton {
public static Singleton singleton = new Singleton();
//讲构造方法私有,防止外接构造Singleton实例
private Singleton() {
}
public static Singleton getInstance() {
return singleton;
}
}
2.饿汉式(静态代码块)【可用】
这种方式和上面的方式其实类似,只不过将类实例化的过程放在了静态代码块中,也是在类装载的时候,就执行静态代码块中的代码,初始化类的实例。优缺点和上面是一样的。
public class Singleton {
private static Singleton singleton;
static {
singleton = new Singleton();
}
//讲构造方法私有,防止外接构造Singleton实例
private Singleton() {
}
public static Singleton getInstance() {
return singleton;
}
}
3.懒汉式(线程不安全)【不可用】
这种写法起到了Lazy Loading的效果,但是只能在单线程下使用。如果在多线程下,一个线程进入了if (singleton == null)判断语句块,还未来得及往下执行,另一个线程也通过了这个判断语句,这时便会产生多个实例。所以在多线程环境下不可使用这种方式。
public class Singleton {
private static Singleton singleton;
//讲构造方法私有,防止外接构造Singleton实例
private Singleton() {
}
public static Singleton getInstance() {
if(singleton == null) {
singleton = new Singleton();
}
return singleton;
}
}
4.懒汉式(线程安全,同步方法)【不推荐用】
解决上面第三种实现方式的线程不安全问题,做个线程同步就可以了,于是就对getInstance()方法进行了线程同步。 缺点:效率太低了,每个线程在想获得类的实例时候,执行getInstance()方法都要进行同步。而其实这个方法只执行一次实例化代码就够了,后面的想获得该类实例,直接return就行了。方法进行同步效率太低要改进。
public class Singleton {
private static Singleton singleton;
//讲构造方法私有,防止外接构造Singleton实例
private Singleton() {
}
public static synchronized Singleton getInstance() {
if(singleton == null) {
singleton = new Singleton();
}
return singleton;
}
}
5.懒汉式(线程安全,同步代码块)【不可用】
由于第四种实现方式同步效率太低,所以摒弃同步方法,改为同步产生实例化的的代码块。但是这种同步并不能起到线程同步的作用。跟第3种实现方式遇到的情形一致,假如一个线程进入了if (singleton == null)判断语句块,还未来得及往下执行,另一个线程也通过了这个判断语句,这时便会产生多个实例。
public class Singleton {
private static Singleton singleton;
//讲构造方法私有,防止外接构造Singleton实例
private Singleton() {
}
public static synchronized Singleton getInstance() {
if(singleton == null) {
synchronized (Singleton.class) {
singleton = new Singleton();
}
}
return singleton;
}
}
6.双重检查【推荐使用】
Double-Check概念对于多线程开发者来说不会陌生,如代码中所示,我们进行了两次if (singleton == null)检查,这样就可以保证线程安全了。这样,实例化代码只用执行一次,后面再次访问时,判断if (singleton == null),直接return实例化对象。 优点:线程安全;延迟加载;效率较高。
public class Singleton {
private static Singleton singleton;
//讲构造方法私有,防止外接构造Singleton实例
private Singleton() {
}
public static synchronized Singleton getInstance() {
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
}
7.静态内部类【推荐使用】
这种方式跟饿汉式方式采用的机制类似,但又有不同。两者都是采用了类装载的机制来保证初始化实例时只有一个线程。不同的地方在饿汉式方式是只要Singleton类被装载就会实例化,没有Lazy-Loading的作用,而静态内部类方式在Singleton类被装载时并不会立即实例化,而是在需要实例化时,调用getInstance方法,才会装载SingletonInstance类,从而完成Singleton的实例化。 类的静态属性只会在第一次加载类的时候初始化,所以在这里,JVM帮助我们保证了线程的安全性,在类进行初始化时,别的线程是无法进入的。 优点:避免了线程不安全,延迟加载,效率高。
public class Singleton {
private static Singleton singleton;
private static class SingletonInstance {
private static final Singleton INSTANCE = new Singleton();
}
public static Singleton getInstance() {
return SingletonInstance.INSTANCE;
}
}
8.枚举【推荐使用】
借助JDK1.5中添加的枚举来实现单例模式。不仅能避免多线程同步问题,而且还能防止反序列化重新创建新的对象。
public enum SingletonEnum {
INSTANCE01, INSTANCE02;
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
9.单例模式和线程安全
- 预加载只有一条语句return instance,这显然可以保证线程安全。但是,我们知道预加载会造成内存的浪费。
- 懒加载不浪费内存,但是无法保证线程的安全。首先,if判断以及其内存执行代码是非原子性的。其次,new Singleton()无法保证执行的顺序性。
不满足原子性或者顺序性,线程肯定是不安全的,这是基本的常识,不再赘述。我主要讲一下为什么new Singleton()无法保证顺序性。我们知道创建一个对象分三步:
memory=allocate();//1:初始化内存空间
ctorInstance(memory);//2:初始化对象
instance=memory();//3:设置instance指向刚分配的内存地址
jvm为了提高程序执行性能,会对没有依赖关系的代码进行重排序,上面2和3行代码可能被重新排序。我们用两个线程来说明线程是不安全的。线程A和线程B都创建对象。其中,A2和A3的重排序,将导致线程B在B1处判断出instance不为空,线程B接下来将访问instance引用的对象。此时,线程B将会访问到一个还未初始化的对象(线程不安全)。
| 时间 | 线程A | 线程B |
|---|---|---|
| t1 | A1:分配对象的内存空间 | |
| t2 | A3:设置instanc指向内存空间 | |
| t3 | B1:判断instance是否为空 | |
| t4 | B2:由于instance不为null,线程B将访问instance引用的对象 | |
| t5 | A2:初始化对象 | |
| t6 | A4:访问instance引用的对象 |
保证懒加载的线程安全
我们首先想到的就是使用synchronized关键字。synchronized加载getInstace()函数上确实保证了线程的安全。**但是,如果要经常的调用getInstance()方法,不管有没有初始化实例,都会唤醒和阻塞线程。**为了避免线程的上下文切换消耗大量时间,如果对象已经实例化了,我们没有必要再使用synchronized加锁,直接返回对象。
public class Singleton {
private static Singleton instance = null;
private Singleton() {
}
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
我们把sychronized加在if(instance==null)判断语句里面,保证instance未实例化的时候才加锁
public class Singleton {
private static Singleton instance = null;
private Singleton() {
}
public static synchronized Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
我们经过上面的讨论知道new一个对象的代码是无法保证顺序性的,因此,我们需要使用另一个关键字volatile保证对象实例化过程的顺序性。
public class Singleton {
private static volatile Singleton instance = null;
private Singleton() {
}
public static synchronized Singleton getInstance() {
if (instance == null) {
synchronized (instance) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
volatile
我们详细讲下volatile的作用
王小明: volatile 除了让共享变量具有可见性,还具有有序性(禁止指令重排序)。
面试官: 你先跟我举几个实际volatile 实际项目中的例子?
王小明: 可以的。有个特别常见的例子:
状态标志
比如我们工程中经常用一个变量标识程序是否启动、初始化完成、是否停止等,代码如下:
//新消息检查处理
public class MessageLoopHandler {
//volatile 变量作为状态标识
private volatile boolean shutdown = false;
//A线程执行shutdown方法,shutdown变量变为true
public void shutdown() {
shutdown = true;
}
//B线程检查到shutdown为true,结束while循环
public void doWork() {
while (!shutdown) {
}
}
}
volatile 很适合只有一个线程修改,其他线程读取的情况。volatile 变量被修改之后,对其他线程立即可见。
面试官: 现在我们来看一下你的例子,如果不加volatile 修饰,会有什么后果?
王小明: 比如这是一个带前端交互的系统,有A、 B二个线程,用户点了停止应用按钮,A 线程调用shutdown() 方法,让变量shutdown 从false 变成 true,但是因为没有使用volatile 修饰, B 线程可能感知不到shutdown 的变化,而继续执行 doWork 内的循环,这样违背了程序的意愿:当shutdown 变量为true 时,代表应用该停下了,doWork函数应该跳出循环,不再执行。
面试官: volatile还有别的应用场景吗?
王小明: 【心里炸了,举单例模式例子简直给自己挖坑】这三个问题,我来一个个回答:
1.为什么使用volatile 修饰了singleton 引用还用synchronized 锁?
volatile只保证了共享变量singleton的可见性,但是 singleton = new Singleton(); 这个操作不是原子的,可以分为三步:
- 步骤1:在堆内存申请一块内存空间;
- 步骤2:初始化申请好的内存空间;
- 步骤3:将内存空间的地址赋值给 singleton;
所以singleton = new Singleton(); 是一个由三步操作组成的复合操作,多线程环境下A 线程执行了第一步、第二步之后发生线程切换,B 线程开始执行第一步、第二步、第三步(因为A 线程singleton 是还没有赋值的),所以为了保障这三个步骤不可中断,可以使用synchronized 在这段代码块上加锁。
2.第一次检查singleton 为空后为什么内部还进行第二次检查?
A 线程进行判空检查之后开始执行synchronized代码块时发生线程切换(线程切换可能发生在任何时候),B 线程也进行判空检查,B线程检查 singleton == null 结果为true,也开始执行synchronized代码块,虽然synchronized 会让二个线程串行执行,如果synchronized代码块内部不进行二次判空检查,singleton 可能会初始化二次。
3.volatile 除了内存可见性,还有别的作用吗?
volatile 修饰的变量除了可见性,还能防止指令重排序。 指令重排序 是编译器和处理器为了优化程序执行的性能而对指令序列进行重排的一种手段。现象就是CPU 执行指令的顺序可能和程序代码的顺序不一致,例如 a = 1; b = 2; 可能 CPU 先执行b=2; 后执行a=1; singleton = new Singleton(); 由三步操作组合而成,如果不使用volatile 修饰,可能发生指令重排序。步骤3 在步骤2 之前执行,singleton 引用的是还没有被初始化的内存空间,别的线程调用单例的方法就会引发未被初始化的错误。
指令重排序也遵循一定的规则:
- 重排序不会对存在依赖关系的操作进行重排
- 重排序目的是优化性能,不管怎样重排,单线程下的程序执行结果不会变
volatile使用总结
volatile 是Java 提供的一种轻量级同步机制,可以保证共享变量的可见性和有序性(禁止指令重排),常用于状态标志、双重检查的单例等场景。使用原则:
- 对变量的写操作不依赖于当前值。例如 i++ 这种就不适用。
- 该变量没有包含在具有其他变量的不变式中。
- volatile的使用场景不是很多,使用时需要仔细考虑下是否适用volatile,注意满足上面的二个原则。
- 单个的共享变量的读/写(比如a=1)具有原子性,但是像num++或者a=b+1;这种复合操作,volatile无法保证其原子性
五.单例模式的优点
系统内存中该类只存在一个对象,节省了系统资源,对于一些需要频繁创建销毁的对象,使用单例模式可以提高系统性能。
六.单例模式的缺点
当想实例化一个单例类的时候,必须要记住使用相应的获取对象的方法,而不是使用new,可能会给其他开发人员造成困扰,特别是看不到源码的时候。
七.单例模式的使用场景
- 需要频繁的进行创建和销毁的对象;
- 创建对象时耗时过多或耗费资源过多,但又经常用到的对象;
- 工具类对象;
- 频繁访问数据库或文件的对象。
八.特殊情况:
8.1当使用反射创建实例时还能保证单例吗?
1、懒汉模式:无解
2、饿汉模式
解决方法:把构造方法增加判断,如果使用反射调用私有构造方法,则会抛异常。
//不允许被实例化
private HunglySingleton(){
if(instance!=null){
throw new RuntimeException("单例不允许多例实例");
}
}
3、静态内部类模式
同饿汉模式的解决办法
4、enmu模式
天然的,就不能被反射调用创建实例,当使用反射newInstance()就会抛异常。
8.2当使用序列化反序列化创建实例时还能保证单例吗?
当我们对静态内部类模式测试一下,序列化并反序列化后,还是同一个实例吗?
public class InnerSingletonTest {
public static void main(String[] args) throws IOException, ClassNotFoundException {
InnerSingleton instance = InnerSingleton.getInstance();
ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream("innerSingletonObject"));
objectOutputStream.writeObject(instance);
objectOutputStream.close();
ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream("innerSingletonObject"));
InnerSingleton instance2 = (InnerSingleton)objectInputStream.readObject();
objectInputStream.close();
System.out.println(instance==instance2);
}
}
class InnerSingleton implements Serializable {
private static class InnerSingletonHolder {
private static InnerSingleton instance= new InnerSingleton();
}
//不允许被实例化
private InnerSingleton(){
if(InnerSingletonHolder.instance!=null){
throw new RuntimeException("单例不允许多例实例");
}
}
/**
* 获取实例
* @return
*/
public static InnerSingleton getInstance() {
return InnerSingletonHolder.instance;
}
}
答案是false。因为反序列化没有调用InnerSingleton的构造方法。
解决办法:
查看serializable接口的说明如下 需要增加一个readResolve方法
Object readResolve() throws ObjectStreamException{
return getInstance();
}
九.jdk和spring中单例模式的应用:
jdk:Runtime类(饿汉)、Currency spring:DefaultSingletonBeanRegistry、ReactiveAdapterRegistry、ProxyFactoryBean(aop包)