专业编程基础技术教程

网站首页 > 基础教程 正文

你真的了解单例模式吗(单例模式介绍)

ccvgpt 2024-07-28 12:11:55 基础教程 9 ℃

今天我们来看一下单例模式,如何颠覆我们的认知。


单例模式(Singleton pattern),属于创建类型的一种常用的软件设计模式。通过单例模式的方法创建的类在当前进程中只有一个实例(根据需要,也有可能一个线程中属于单例,如:仅线程上下文内使用同一个实例)

你真的了解单例模式吗(单例模式介绍)

先告诉大家单例模式有以下这些,我们来看看它是如何一步一步演化的吧!

  1. 饿汉式单例
  2. 懒汉式单例
  3. 注册式单例
  4. 本地线程单例

饿汉式单例


我们熟知的饿汉式单例是这样的

/**
 * @author ZerlindaLi create at 2020/9/4 11:20
 * @version 1.0.0
 * @description HungrySingleton
 * 饿汉模式
 * 优点:代码简单
 * 缺点:线程不安全,当单例很多时容易造成内存泄露
 */
public class HungrySingleton {

    private static final HungrySingleton hungrySingleton = new HungrySingleton();
    private HungrySingleton(){}

    public HungrySingleton getInstance(){
        return hungrySingleton;
    }

}

或者这样

public class HungryStaticSingleton {

    private static HungryStaticSingleton instance;
    static{
        instance = new HungryStaticSingleton();
    }

    public HungryStaticSingleton getInstance(){
        return instance;
    }
}

懒汉式单例


饿汉式单例在类加载时就会创建单例对象,当程序中有很多这样的单例时,又没有去调用,就会造成内存浪费。为了解决内存浪费这个问题,我们可以将实例延迟加载,当程序需要的时候,才实例化,就产生了下面的简单懒汉式单例

public class LazySimpleSingleton {

    private static LazySimpleSingleton lazySimpleSingleton;
    private LazySimpleSingleton(){}

    public static synchronized LazySimpleSingleton getInstance(){
        if(lazySimpleSingleton == null){
            lazySimpleSingleton = new LazySimpleSingleton();
        }
        return lazySimpleSingleton;
    }

}

为了避免线程安全问题,我们在方法上加了锁,但是这个会造成阻塞,只要请求这个方法,就要排队等候。那我们把锁加到方法里面只有实例对象为null时,才去创建,就是下面这个样子的

public class LazySimpleSingleton {

    private static LazySimpleSingleton lazySimpleSingleton;
    private LazySimpleSingleton(){}

    public static  LazySimpleSingleton getInstance(){
        if(lazySimpleSingleton == null){
            synchronized (LazyStaticInnerClassSingleton.class){
                lazySimpleSingleton = new LazySimpleSingleton();
            }
        }
        return lazySimpleSingleton;
    }

}

我们仔细看看,发现这样依然不能解决线程安全问题。我们两个线程同时走到if判断,此时结果都为true, 那么他们都会执行锁以及所里面的问题。那我们想到,在执行锁里面的实例化过程之前,我们可以再加上一个判断,判断对象是否为空。就是下面这个代码,双重检查

public class LazyDoubleCheckSingleton {

    private volatile static LazyDoubleCheckSingleton instance;
    private LazyDoubleCheckSingleton(){}

    public static LazyDoubleCheckSingleton getInstance(){
        // 检查是否要阻塞
        if(instance==null){
            synchronized (LazyDoubleCheckSingleton.class){
                // 检查是否要重新创建实例
                if(instance == null){
                    instance = new LazyDoubleCheckSingleton();
                    // 指令重排序的问题,所以使用了volatile
                }
            }
        }
        return instance;
    }
}

我们看这个代码既解决了内存浪费和线程安全问题,有避免了阻塞。但是它不够优雅。我们有一种更优雅的写法,钻个java语法的空子,利用内部内的方式。来看一下代码

/**
 * @author ZerlindaLi create at 2020/9/4 16:08
 * @version 1.0.0
 * @description LazyStaticInnerClassSingleton
 * 优点:内部类使用时才会被jvm加载,初始化静态成员变量
 * 缺点:会被反射破坏
 */
public class LazyStaticInnerClassSingleton {

    private LazyStaticInnerClassSingleton(){}

    public static LazyStaticInnerClassSingleton getInstance(){
        return LazyHolder.INSTANCE;
    }

    private static class LazyHolder{
        private static final LazyStaticInnerClassSingleton INSTANCE = new LazyStaticInnerClassSingleton();
    }
}

这个代码看似已经完美了,但是它会被发射破坏。我们来测试一下

/**
 * @author ZerlindaLi create at 2020/9/4 16:28
 * @version 1.0.0
 * @description ReferTest
 */
public class ReflectTest {
    public static void main(String[] args) {
        try{
            Class<?> clazz = LazyStaticInnerClassSingleton.class;
            Constructor c = clazz.getDeclaredConstructor(null);
            c.setAccessible(true);
            Object o1 = c.newInstance();
            Object o2 = c.newInstance();
            System.out.println(o1);
            System.out.println(o2);
        }catch (Exception e){
            e.printStackTrace();
        }

    }
}

运行结果如图

可以看到,两次通过反射得到了两个不同的实例。那我们在构造器里面判断一下,如果已经存在实例对象,我们就抛出异常

private LazyStaticInnerClassSingleton(){
    if(LazyHolder.INSTANCE!=null){
        throw new RuntimeException("不允许非法访问");
    }
}

这样子通过反射来实例化的结果是什么呢?

我们进入newInstance()方法里面,可以看到样子一行代码

if ((clazz.getModifiers() & Modifier.ENUM) != 0)
    throw new IllegalArgumentException("Cannot reflectively create enum objects");

也就是说,枚举类型不会被反射破坏。

同时,以上所有单例在序列化和反序列化之后,都会得到两个不同的实例。我们来做个试验,用最简单的饿汉式单例

public class SeriableSingleton implements Serializable {

    private static final SeriableSingleton INSTANCE = new SeriableSingleton();
    private SeriableSingleton(){}

    public static SeriableSingleton getInstance() {
        return INSTANCE;
    }
}

做一个序列化的测试

/**
 * @author ZerlindaLi create at 2020/9/7 13:51
 * @version 1.0.0
 * @description SeriableSingletonTest
 */
public class SeriableSingletonTest {
    public static void main(String[] args) {
        SeriableSingleton s1 = SeriableSingleton.getInstance();
        SeriableSingleton s2 = null;
        try {
            // 序列化
            FileOutputStream fos = new FileOutputStream("SeriableSingleton.txt");
            ObjectOutputStream oos = new ObjectOutputStream(fos);
            oos.writeObject(s1);
            // 关闭流
           oos.flush();
            oos.close();
            // 反序列化
            FileInputStream fis = new FileInputStream("SeriableSingleton.txt");
            ObjectInputStream ois = new ObjectInputStream(fis);
            s2 = (SeriableSingleton)ois.readObject();
            ois.close();
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }

        System.out.println(s1);
        System.out.println(s2);
        System.out.println(s1 == s2);
    }
}

执行结果如下图

很明显得到了两个不同的对象。为了解决序列化的问题,我们走进readObject()方法来看一看。里面有一个readObject0(),找到类型为对象的

case TC_OBJECT:
    return checkResolve(readOrdinaryObject(unshared));

我们来看看readOrdinaryObject(unshared)里面做了什么

obj = desc.isInstantiable() ? desc.newInstance() : null;

判断了是否有无参构造器,有就创建一个实例

boolean isInstantiable() {
    requireInitialized();
    return (cons != null);
}

继续往下看,会看到这个

if (obj != null &&
    handles.lookupException(passHandle) == null &&
    desc.hasReadResolveMethod())
{
    Object rep = desc.invokeReadResolve(obj);
    if (unshared && rep.getClass().isArray()) {
        rep = cloneArray(rep);
    }
    if (rep != obj) {
        // Filter the replacement object
        if (rep != null) {
            if (rep.getClass().isArray()) {
                filterCheck(rep.getClass(), Array.getLength(rep));
            } else {
                filterCheck(rep.getClass(), -1);
            }
        }
        handles.setObject(passHandle, obj = rep);
    }
}

这里判断了是否有readResolve()方法,如果有,就获取这个方法返回的对象,并且通过一些列处理之后赋值。那我们可以在类里面加上这个方法,并且将单例返回,如下

public class SeriableSingleton implements Serializable {

    private static final SeriableSingleton INSTANCE = new SeriableSingleton();
    private SeriableSingleton(){}

    public static SeriableSingleton getInstance() {
        return INSTANCE;
    }


    private Object readResolve(){
        return INSTANCE;
    }
}

注册式单例


前面我们说了枚举可以完美的避免反射破坏,那么是否也可以避免序列化破坏呢?下面我们可以通过枚举来完成单例模式

/**
 * @author ZerlindaLi create at 2020/9/4 17:39
 * @version 1.0.0
 * @description EnumSingleton
 * 枚举式单例:
 * 反射:在newInstance()方法里面,我们可以看到官方禁止了通过反射实例化枚举
 *
 * 序列化:我们在readObject方法里面还是可以看到实际调用的是Enum的valueOf方法。而这个方法是通过map来取值的。
 *     public static <T extends Enum<T>> T valueOf(Class<T> enumType,
 *                                                 String name) {
 *         T result = enumType.enumConstantDirectory().get(name);
 *         if (result != null)
 *             return result;
 *         if (name == null)
 *             throw new NullPointerException("Name is null");
 *         throw new IllegalArgumentException(
 *             "No enum constant " + enumType.getCanonicalName() + "." + name);
 *     }
 */
public enum EnumSingleton{
    INSTANCE;
    private Object data;
    public Object getData() {
        return data;
    }

    public void setData(Object data) {
        this.data = data;
    }

    public static EnumSingleton getInstance(){
        return INSTANCE;
    }
}

我们来测一下反射的结果

正如我们前面看到的,java官方不允许通过反射实例化枚举类型。

再来测试一下能否被序列化破坏

如上图所示,并没有。

我们针对枚举是怎么进行序列化的呢。依然进入readObject()方法,找到readObject0(),找到TC_ENUM,进入readEnum(unshared)

可以看到,它是通过Enum.valueOf((Class)cl, name)来得到一个实例的。我们进入valueOf来看看

注意到这里有一个enumConstantDirectory()方法,来看看

这个方法返回的是一个enumConstantDirectory对象

private volatile transient Map<String, T> enumConstantDirectory = null;

这是我们终于揭开它的神秘面纱了,它就是一个volatile和transient修饰的Map, 序列化时是通过类名和类对象类来找对唯一对应的枚举对象的。因此枚举对象不会被类加载器加载多次,所以它不会被序列化破坏。

但是我们细品,枚举类是不是在类初始化时就被jvm加载了,就实例化枚举对象了?那这又回到饿汉式单例的问题了,依旧会造成内存浪费,不适合创建大量单例的场景。那我们是否可以利用这个思路,用map自己来实现一个延迟加载的单例呢?

我们来看一下容器式单例模式

/**
 * @author ZerlindaLi create at 2020/9/4 17:39
 * @version 1.0.0
 * @description ContainerSingleton
 * Spring框架中单例的应用,ioc容器
 */
public class ContainerSingleton {
    // 私有化构造器
    private ContainerSingleton(){}

    private static Map<String, Object> ioc = new ConcurrentHashMap<>();
    public static Object getBean(String className){
        if (!ioc.containsKey(className)) {
            synchronized (ioc){
                if(!ioc.containsKey(className)){
                    Object obj = null;
                    try {
                        obj = Class.forName(className).newInstance();
                        ioc.put(className, obj);
                    } catch (ClassNotFoundException e) {
                        e.printStackTrace();
                    } catch (IllegalAccessException e) {
                        e.printStackTrace();
                    } catch (InstantiationException e) {
                        e.printStackTrace();
                    }
                    return obj;
                }else{
                    return ioc.get(className);
                }
            }

        } else {
            return ioc.get(className);
        }
    }

}

这里我们依然使用了双重锁机制来确保线程安全,又回到了前面的代码不优雅问题。所以到底哪种单例最好呢?我们要根据具体的场景,对比每种单例的优缺点,来选择合适的单例。

Spring容器其实也是一个单例模式,它也是将实例存放在一个map里面的,我们来看看Spring是怎么确保线程安全的

Threadlocal式单例


最后给大家一个彩蛋,本地线程式单例,天生的线程安全

/**
 * @author ZerlindaLi create at 2020/9/4 17:46
 * @version 1.0.0
 * @description ThreadlocalSingleton
 * 不能保证其创建的对象全局唯一,但能保证在单个线程中是唯一的,天生是线程安全的
 */
public class ThreadlocalSingleton {

    private static ThreadLocal<ThreadlocalSingleton> threadLocal = ThreadLocal.withInitial(() -> new ThreadlocalSingleton());
    private ThreadlocalSingleton (){}

    public static ThreadlocalSingleton getInstance(){
        return threadLocal.get();
    }
}

大家可以尝试自己写一个测试类,来看看ThreadlocalSingleton是否是在单个线程里面线程安全。同时Threadlocal还有一个可以利用的点,就是能其他隔离做了。

看似简单的单例原来有这么多的门道了,看了这篇文章是否颠覆了你的认知呢?喜欢我请给我点一个再看,我会带来更多好内容!

Tags:

最近发表
标签列表