单例模式
在有些系统中,为了节省内存资源、保证数据内容的一致性,对某些类要求只能创建一个实例,这就是所谓的单例模式。
定义
单例(Singleton)模式的定义:指一个类只有一个实例,且该类能自行创建这个实例的一种模式
使用场景
- 业务系统中全局只需要一个对象实例⽐如发号器(比如mysql的自增id)、redis连接对象(在配置中建立一个连接,不需要在使用的时候再建立连接,耗费性能)等
- Spring IOC容器中的bean默认就是单例
- Spring boot中controller,service,dao层中的@autowire的依赖注入对象都是单例
单例的分类(懒汉与饿汉)
- 懒汉:懒加载,需要用到的时候创建对象,延迟加载
- 饿汉:提前创建对象
实现步骤
- 私有化构造函数(普通类创建对象,只需要new一下就行,单例不能把构造暴露出去)
- 提供获取单例的方法(将构造好的对象暴露访问接口)
懒汉单例代码实现
第一种懒汉单例构造
public class LazySingleton {
private static LazySingleton instance;
//构造函数私有化
private LazySingleton(){
System.out.println("构造对象");
}
/**
* @Description 对外暴露一个方法获取类的对象
*/
public static LazySingleton getInstance(){
if(instance == null){
instance = new LazySingleton();
}
return instance;
}
}
这种存在线程不安全问题,如果多线程情况下,线程A和线程B同时判断到instance为null,线程A和线程B都会创建一个对象,后一个创建的覆盖前一个,那这就不能保证是单例了。
第二种懒汉单例构造,方法加锁
/**
* 第二种方式加锁
* 通过加锁 synchronized 保证单例
* 采用synchronized对方法加锁 有很大的性能开销
*/
public static synchronized LazySingleton getInstance(){
if(instance == null){
instance = new LazySingleton();
}
return instance;
}
采用synchronized对方法加锁 有很大的性能开销,因此锁粒度改小
第三种实现方式 双重检查锁定DCL
/**
* 第三种实现方式 双重检查锁定DCL (Double-CheckedLocking) ,在多线程情况下保持高性能
*
*/
public static LazySingleton getInstance(){
//多线程情况下,线程A和B都判断第一次instance == null,加锁只能保A和B不能同时创建对象。但是假如A拿到锁后创建对象后,B再拿到锁创建对象,B创建的对象覆盖A创建的,所以需要再一次判断instance == null。这就是DCL
if(instance == null){
synchronized(LazySingleton.class){
if(instance == null){
instance = new LazySingleton();
}
}
}
return instance;
}
多线程情况下,线程A和B都判断第一次instance == null,加锁只能保A和B不能同时创建对象。但是假如A拿到锁后创建对象后,B再拿到锁创建对象,B创建的对象覆盖A创建的,所以需要再一次判断instance == null。这就是DCL
第三种方法依旧不安全,instance == new LazySingleton();不是原子操作。创建对象需要三个操作。
- 1.分配空间给对象
- 2.在空间内创建对象
- 3.将对象赋值给引用instance
极端情况下,假如创建对象的步骤是1-》3-》2。执行3的时候,其他线程就会读取instance最新的值。但是这个时候对象是不安全的。(这个涉及指令重排的知识)
第四种 voliate 解决指令重排问题
/**
* 创建对象顺序
* - 1.分配空间给对象
* - 2.在空间内创建对象
* - 3.将对象赋值给引用instance
*
* 极端情况下,假如创建对象的步骤是1-》3-》2。执行3的时候,其他线程就会读取instance最新的值。但是这个时候对象是不安全的。(这个涉及指令重排的知识)
*/
private static volatile LazySingleton instance;
/**
* 第三种实现方式 双重检查锁定DCL (Double-CheckedLocking) ,在多线程情况下保持⾼高性能
*/
public static LazySingleton getInstance(){
if(instance == null){
synchronized(LazySingleton.class){
if(instance == null){
instance = new LazySingleton();
}
}
}
return instance;
}
饿汉单例代码实现
public class HungrySingleton {
//在类加载的时候就创建了
private static HungrySingleton instance = new HungrySingleton();
//构造方法私有化
private HungrySingleton(){}
//对外暴露一个方法获取类的对象
public static HungrySingleton getInstance(){
return instance;
}
}
懒汉饿汉选择
- 懒汉代码实现麻烦,饿汉代码实现简单
- 饿汉不存在多线程同步问题
- 饿汉提前创建对象,会一直占着内存
- 在对象不大,创建不复杂的情况下,直接使用饿汉单例
JDK源码中单例的应用
饿汉模式
Runtime runtime = Runtime.getRuntime();
懒汉模式
Console console = System.console();
序列化破坏单例
LazySingleton instance = LazySingleton.getInstance();
LazySingleton instance2 = null;
//Write Obj to file
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("tempFile"));
oos.writeObject(instance);
//Read Obj from file
File file = new File("tempFile");
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file));
instance2 = (LazySingleton) ois.readObject();
System.out.println(instance == instance2);
当单例implements Serializable,我们可以通过文件的反序列化获取对象,获取的对象.从上图的false说明经过I/O操作后,对象的状态发生了变化。也就是说对象在序列化与反序列化时出现了问题。
解决办法
//在单例中加入
private Object readResolve() {
return instance;
}
再跑一次结果
反射破坏单例
LazySingleton instance = LazySingleton.getInstance();
Constructor<?> constructor = LazySingleton.class.getDeclaredConstructor();
constructor.setAccessible(true);
LazySingleton instance2 = (LazySingleton) constructor.newInstance();
System.out.println(instance == instance2);
结果如下
构造方法执行了两遍。
解决方法
private LazySingleton(){
//解决反射导致的破坏单例
if (instance!=null){
throw new RuntimeException("单例不允许多实例");
}
System.out.println("构造对象");
}
再一次执行结果如下
附源码:gitee源码地址