04.创建型 - 单例模式(Singleton Pattern)
单例模式 (Singleton Pattern)
关于单例模式其实没什么好说的, 只要是个程序员应该基本都接触过, 编写过单例对象.
参考 - https://www.runoob.com/design-pattern/singleton-pattern.html
这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。
它确保一个类只有一个实例,并提供了一个全局访问点来访问该实例。
- 保证一个类只有一个实例
- 为该实例提供一个全局访问节点
急加载 静态代码 实现
由jvm线程 保证线程安全
public class ConnectHolder {
private static final ConnectHolder instance = new ConnectHolder();
/*** 或者
private static ConnectHolder instance = null
static {
instance = new ConnectHolder();
}
**/
private ConnectHolder(){}
...
}
懒加载 双重校验 实现
-
在声明变量时使用了 volatile 关键字,其作用有两个:
保证变量的可见性:当一个被volatile关键字修饰的变量被一个线程修改的时候,其他线程可以立刻得到修改之后的结果。
屏蔽指令重排序:指令重排序是编译器和处理器为了高效对程序进行优化的手段,它只能保证程序执行的结果时正确的,但是无法保证程序的操作顺序与代码顺序一致。这在单线程中不会构成问题,但是在多线程中就会出现问题。 -
将同步方法改为同步代码块. 在同步代码块中使用二次检查,以保证其不被重复实例化 同时在调用getInstance()方法时不进行同步锁,效率高。
volatile关键词 很重要, JVM可能优化指令重排序在类加载时,'对象指针'赋值 优先对象初始化
public class Singleton {
//1. 私有构造方法
private Singleton(){
}
//2. 在本类中创建私有静态的全局对象
// 使用 volatile保证变量可见性,屏蔽指令重排序
private volatile static Singleton instance;
//3. 获取单例对象的静态方法
public static Singleton getInstance(){
//第一次判断,如果instance不为null,不进入抢锁阶段,直接返回实例
if(instance == null){
synchronized (Singleton.class){
//第二次判断,抢到锁之后再次进行判断,判断是否为null
if(instance == null){
instance = new Singleton();
/**
* 上面的创建对象的代码,在JVM中被分为三步:
* 1.分配内存空间
* 2.初始化对象
* 3.将instance指向分配好的内存空间
*/
}
}
}
return instance;
}
}
在java内存模型中,volatile 关键字作用可以是保证可见性或者禁止指令重排。这里是因为 singleton = new Singleton() ,它并非是一个原子操作,事实上,在 JVM 中上述语句至少做了以下这 3 件事:
- 第一步 是给 singleton 分配内存空间;
- 第二步 开始调用 Singleton 的构造函数等,来初始化 singleton;
- 第三步 将 singleton 对象指向分配的内存空间(执行完这步 singleton 就不是 null 了)。
这里需要留意一下 1-2-3 的顺序,因为存在指令重排序的优化,也就是说第 2 步和第 3 步的顺序是不能保证的,最终的执行顺序,可能是 1-2-3,也有可能是 1-3-2。
如果是 1-3-2,那么在第 3 步执行完以后,singleton 就不是 null 了,可是这时第 2 步并没有执行,singleton 对象未完成初始化,它的属性的值可能不是我们所预期的值。假设此时线程 2 进入 getInstance 方法,由于 singleton 已经不是 null 了,所以会通过第一重检查并直接返回,但其实这时的 singleton 并没有完成初始化,所以使用这个实例的时候会报错.
in short 因为jvm指令重排序, 导致未执行第二步, 返回未初始化的对象
懒加载 静态内部类 实现
/**
* 单例模式-静态内部类(懒加载) 根据静态内部类的特性,同时解决了 延时加载 线程安全的问题,并且代码更加简洁
**/
public class Singleton {
//创建静态内部类
private static class SingletonHandler{
//在静态内部类中创建单例,在装载内部类的时候,才会创建单例对象
private static Singleton instance = new Singleton();
}
public static Singleton getInstance(){
return SingletonHandler.instance;
}
}
破坏单例模式
- 反射机制
JDK的反射机制, 可以获取到任意类的java.lang.reflect.Constructor构造方法, 创建实例 从而会破坏单例模式
解决:
可以在构造方法使用代码判断, 如果 instance != null 忽略或抛出异常
public class Singleton {
private Singleton(){
if(SingletonHandler.instance != null){//解决反射机制破坏单例
throw new RuntimeException("不允许非法访问");
}
}
//创建静态内部类
private static class SingletonHandler{
//在静态内部类中创建单例,在装载内部类的时候,才会创建单例对象
private static Singleton instance = new Singleton();
}
public static Singleton getInstance(){
return SingletonHandler.instance;
}
}
- 序列化机制
JDK自带的ObjectOutputStream序列化 和ObjectInputStream反序列化机制
//序列化
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("obj.dat"));
oos.writeObject(taskConfig);
//反序列化
ObjectInputStream ois = new ObjectInputStream(new FileOutputStream("obj.dat"));
taskConfig = (TaskConfig) ois.readObject();
解决:
ObjectInputStream 反序列化机制会读取对象是否有 Object readResolve(){} 方法, 如果有会使用通过反射调用该方法, 获取到实例
所以编写一个该方法, 返回该单例实例即可解决
public class Singleton {
private Singleton(){
if(SingletonHandler.instance != null){//解决反射机制破坏单例
throw new RuntimeException("不允许非法访问");
}
}
private Object readResolve() {
return INSTANCE; // 返回当前已存在的单例实例, 解决 ObjectInputStream 反序列化机制破坏单例
}
//创建静态内部类
private static class SingletonHandler{
//在静态内部类中创建单例,在装载内部类的时候,才会创建单例对象
private static Singleton instance = new Singleton();
}
public static Singleton getInstance(){
return SingletonHandler.instance;
}
}
基于枚举 实现
基于枚举的方式实现单例, 天然解决以上问题.
/**
* 单例模式-枚举
* 阻止反射的破坏: 在反射方法中不允许使用反射创建枚举对象
* 阻止序列化的破坏: 在序列化的时候仅仅是将枚举对象的name属性输出到了结果中,反序列化的时候,就会通过
* Enum的 valueOf方法 来根据名字去查找对应枚举对象.
**/
public enum Singleton {
INSTANCE;
private Object data;
public Object getData() {
return data;
}
public void setData(Object data) {
this.data = data;
}
public static Singleton getInstance(){
return INSTANCE;
}
}

浙公网安备 33010602011771号