04.创建型 - 单例模式(Singleton Pattern)

单例模式 (Singleton Pattern)

关于单例模式其实没什么好说的, 只要是个程序员应该基本都接触过, 编写过单例对象.

参考 - https://www.runoob.com/design-pattern/singleton-pattern.html

这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。
它确保一个类只有一个实例,并提供了一个全局访问点来访问该实例。

  1. 保证一个类只有一个实例
  2. 为该实例提供一个全局访问节点

急加载 静态代码 实现

由jvm线程 保证线程安全

public class ConnectHolder {
   private static final ConnectHolder instance = new ConnectHolder();

   /*** 或者
   private static ConnectHolder instance = null
   static {
	   instance = new ConnectHolder();
   }
   **/
   private ConnectHolder(){}
...
}

懒加载 双重校验 实现

  1. 在声明变量时使用了 volatile 关键字,其作用有两个:
    保证变量的可见性:当一个被volatile关键字修饰的变量被一个线程修改的时候,其他线程可以立刻得到修改之后的结果。
    屏蔽指令重排序:指令重排序是编译器和处理器为了高效对程序进行优化的手段,它只能保证程序执行的结果时正确的,但是无法保证程序的操作顺序与代码顺序一致。这在单线程中不会构成问题,但是在多线程中就会出现问题。

  2. 将同步方法改为同步代码块. 在同步代码块中使用二次检查,以保证其不被重复实例化 同时在调用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;
    }
}

破坏单例模式

  1. 反射机制
    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;
    }
}
  1. 序列化机制
    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;
    }
}


posted @ 2025-11-11 22:05  daidaidaiyu  阅读(2)  评论(0)    收藏  举报