单例模式详解

119 阅读4分钟

这是我参与11月更文挑战的第13天,活动详情查看:2021最后一次更文挑战

前言

单例模式是一个重要的设计模式,我们在常用的框架中或者其他地方都能看到,今天我们来一起探讨一下这个设计模式

什么时候可以使用单例模式?

当需要一个对象的生命周期可以贯穿一个继承,并且只需要一个

如:线程池、连接池、SpringIOC容器......

什么是单例模式?

单例模式 :一个单一的类,负责创建自己的对象,同时确保系统中只有单个对象被创建

单例模式的特点:

  1. 某个类只能有一个实例(构造器私有化)
  2. 必须自己创建实例对象(自己实现实例化)
  3. 向外界提供自己的实例(对外提供实例化方法)

饿汉式

package com.cheng.single;

// 饿汉式单例
public class Hungry {

    private final static Hungry HUNGRY = new Hungry(); // 自行创建的对象实例

    // 私有化构造方法
    private Hungry() {
    }
		
    // 提供给外界的获取实例方法
    public static Hungry getInstance(){
        return HUNGRY;
    }
}

饿汉式特点:一旦类加载就把对象实例化,保证了获得实例化时一定存在对象

懒汉式

// 懒汉式单例
public class LazyMan {
    // volatile 避免指令重排
    private volatile static LazyMan lazyMan; 
  
  	private LazyMan() { // 私有化构造器
    }

    public static LazyMan getInstance(){ // 提供给外界的获取实例方法
        // 双重检测锁模式的懒汉式单例 DCL
        if (lazyMan == null){
            synchronized (LazyMan.class){
                if (lazyMan == null){
                    lazyMan = new LazyMan(); // 不是原子性操作
                }
            }
        }
        return lazyMan;
    }
     
}

懒汉式特点:只有当调用获得实例对象方法的时候,才会去初始化这个单例。

懒汉式和饿汉式的区别

创建实例对象的时机

饿汉式:类加载就把对象实例化

懒汉式:当调用获得实例对象方法的时候,才会去初始化这个单例

线程安全

饿汉式:线程安全,因为在类加载的时候实例化对象,所以在多线程中也能保证一个对象实例

懒汉式:懒汉式是非线程安全的,如果有多个线程调用获得实例对象方法的时候可能会创建多个实例对象

可以采用DCL + volatile 来解决这个问题

DCL:双重检测锁模式

 
// volatile 避免指令重排
private volatile static LazyMan lazyMan; 


public static LazyMan getInstance(){ // 提供给外界的获取实例方法
        // 双重检测锁模式的懒汉式单例 DCL
        if (lazyMan == null){
            synchronized (LazyMan.class){
                if (lazyMan == null){
                    lazyMan = new LazyMan(); // 不是原子性操作
                }
            }
        }
        return lazyMan;
    }

反射对单例模式的破坏

我们在学习反射的时候知道了,通过反射我们可以获得类的相关信息,其中就包括类的构造器,通过类的构造器,我们可以创建实例对象,从而破坏单例模式

 // 反射可以破坏单例模式
    public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
       LazyMan instance = LazyMan.getInstance();
            Constructor<LazyMan> declaredConstructor = LazyMan.class.getDeclaredConstructor();
            declaredConstructor.setAccessible(true); // 无视构造方法私有化

            // 可以在构造方法中加锁来解决这个一个问题
            LazyMan lazyMan1 = declaredConstructor.newInstance(); // 创建了第二个对象,破坏了单例模式
    }
image-20211114204223020

解决方法 :可以在构造方法中加锁来解决这个一个问题,如果实例化对象已存在就抛出异常

public LazyMan() {
        synchronized (LazyMan.class){
            if (lazyMan != null){
                throw new RuntimeException("不要试图使用反射来破坏单例模式");
            }
        }
    }

但是还可以直接通过 newInstance() 创建两个对象来避免抛出异常

LazyMan lazyMan1 = declaredConstructor.newInstance();

LazyMan lazyMan2 = declaredConstructor.newInstance();

解决方案:可以设置一个标志位,创建一次后就关闭开关,下次创建检查标志位来抛出异常

// 标注属性
    private static boolean  flag = false; // 标志位

    public LazyMan() {
        synchronized (LazyMan.class){
            if (lazyMan != null || flag){
                throw new RuntimeException("不要试图使用反射来破坏单例模式");
            }
            flag = true;
        }
    }

如何彻底解决这个问题? 使用枚举

从newInstance() 方法源码上看

image-20211114205729866

// enum 是什么?本身也是一个Class类
// 反射不能破坏枚举
public enum EnumSingle {
    INSTANCE;

    public EnumSingle getInstance(){
        return INSTANCE;
    }
}
class Test{
    public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
        EnumSingle enumSingle = EnumSingle.INSTANCE;
        Constructor<EnumSingle> declaredConstructor = EnumSingle.class.getDeclaredConstructor(String.class,int.class);
        declaredConstructor.setAccessible(true);
        EnumSingle enumSingle1 = declaredConstructor.newInstance();
    }
}