设计模式--单例模式的几种实现方式

xiaoxiao2021-02-28  7

1. 概述

单例模式:简单的说就是可以确保只产生一个类实例,让多个用户或者多个线程同时使用这一个实例,而不需要每次使用都创建一次对象。

2. 优缺点和适用场景

单例模式节省了创建对象所需的时间,节约了系统资源,减轻了GC压力。

大部分的单例类的构造器是私有的,这就意味着单例类不容易扩展。

网上说单例的缺点还有就是长时间不使用,会被GC回收,导致对象状态的丢失,其实我不是很认同这点,我觉得单例是不会被GC回收的,毕竟是static类型的,而且始终指向着引用。这个以后具体查一查hotspot的有关实现

适用于需要频繁实例化然后销毁的类或者那些重量级对象,一次创建需要消耗很多系统资源。但是不适用于要保存状态的类,比如:一个订单类,用户A和用户B都有一条订单,A先从数据库中查出订单状态为已付款,这时订单类的状态就变成已付款了,然后B从数据库查出自己的订单状态是未付款的,如果这个订单类是单例的,那么A的订单状态也会变为未付款的,这就乱套了。

单例在多线程的场景中使用要格外小心,包括单例的创建以及共享资源的使用问题。

3. 几种不同形式的单例模式

饿汉式 class SingleClass{ private static SingleClass instance = new SingleClass(); private SingleClass() { System.out.println("single"); } public static SingleClass getInstance() { return instance; } }

这种单例的实现方式非常简单而且可靠,不会涉及到在创建单例时的非线程安全问题,因为实例的创建是在类加载时完成的。但是当这种单例不光要承担创建实例的角色,又要完成其他工作的时候,就有点不那么得心应手了,比如:

class SingleClass{ private static SingleClass instance = new SingleClass(); private SingleClass() { System.out.println("single"); } public static SingleClass getInstance() { return instance; } public static void doSomething(){ ... } }

可以看到,这个单例不光要扮演创建实例的角色又要扮演其他角色(doSomething),当我们调用SingleClass.doSomething()的时候,如果这时类实例还没创建或者说类还没加载,虚拟机在这种场景下就会为我们加载类,并创建实例,然而我们这时并不想让SingleClass产生实例,因为还不需要用到SingleClass的实例。那么有没有一种方式可以延迟加载单例呢,让单例的创建能受我们控制,想让他什么时候创建就什么时候创建,而不是类一加载就创建实例。

懒汉式 class LazySingleClass{ private static LazySingleClass instance = null; private LazySingleClass() { System.out.println("LazySingleClass"); } public static synchronized LazySingleClass getInstance() { if( instance == null) { instance = new LazySingleClass(); } return instance; } public static void doSomething(){ ... } }

当我们调用LazySingleClass.doSomething时,尽管虚拟机会加载类,但是不会创建类实例,因为我们把创建类实例的控制权完全交给了getInstance方法,只有我们调用getInstance时才会创建实例。虽然解决了延迟加载的问题,但是可以看到getInstance方法是加上了同步锁的,因为类实例不是在类加载时完成的,所以肯定涉及到非线程安全问题,当两个线程调用getInstance方法,如果不加上synchronized,一个线程创建完实例前,另一个线程判断instance是空的,这样很容易就创建了两个实例。

getInstance整个方法被加上了同步关键字,这样的效率是很低的,我们可以改良一下,把同步关键字就加在涉及线程安全问题的代码上

DCL class LazySingleClass2{ //这里volatile很重要 private volatile static LazySingleClass2 instance = null; private LazySingleClass2() { System.out.println("LazySingleClass2"); } public static LazySingleClass2 getInstance() { if( instance == null) { synchronized(LazySingleClass2.class){ if( instance == null) { instance = new LazySingleClass2(); } } } return instance; } public static void doSomething(){ System.out.println("..."); } }

双重检测,已经可以做到线程安全了,但要依赖JDK版本,在JDK5.0以后才适用,而且在效率上肯定是比不上饿汉式的。为了解决这个问题,还需要对单例模式进行改进。

使用内部类来实现单例 class InnerSingleClass{ private InnerSingleClass() { System.out.println("InnerSingleClass"); } private static class SingletonHolder{ private static InnerSingleClass instance = new InnerSingleClass(); } public static InnerSingleClass getInstance() { return SingletonHolder.instance; } public static void doSomething(){ System.out.println("..."); } }

当外部类被加载时,内部类不会被初始化,而且将类实例的创建放在内部类加载时完成,避开了非线程安全问题。可以看到这种内部类的实现方式,既满足了延迟加载,又不涉及到非线程安全问题。

以上几种单例模式,的确在大多数情况下能够确保只产生一个实例了,但也有例外的情况,当通过反射,强行调用单例类的私有构造函数,就会产生多个实例,可以对私有构造函数进行异常检测。这种反射造成的问题是一种极端的方式,就不过多去讨论,还有一种情况就是序列化和反序列化的时候会破坏以上单例。

@Test public void test6() throws IOException, ClassNotFoundException { //InnerSingleClass的代码上面有,还有就是InnerSingleClass要继承Serializable接口 InnerSingleClass instance = InnerSingleClass.getInstance(); InnerSingleClass instance2 = null; FileOutputStream fos = new FileOutputStream("single.txt"); ObjectOutputStream oos = new ObjectOutputStream(fos); oos.writeObject(instance); oos.flush(); oos.close(); FileInputStream fis = new FileInputStream("single.txt"); ObjectInputStream ois = new ObjectInputStream(fis); instance2 = (InnerSingleClass) ois.readObject(); System.out.println(instance == instance2); }

这段程序先将InnerSingleClass序列化到文件single.txt,再把它从文件反序列化为对象,序列化前的对象和反序列化的对象理应是同一个对象,然而程序输出为false,事实证明,序列化和反序列化拿到的对象不是同一个单例,那么怎么来避免这一问题发生呢

class InnerSingleClass implements Serializable{ private InnerSingleClass() { System.out.println("InnerSingleClass"); } private static class SingletonHolder{ private static InnerSingleClass instance = new InnerSingleClass(); } public static InnerSingleClass getInstance() { return SingletonHolder.instance; } public static void doSomething(){ System.out.println("..."); } //新加代码 public Object readResolve() { return SingletonHolder.instance; } }

在单例类中新加方法readResolve就可以了,在反序列化时会自动调用readResolve方法。然而还有一种单例模式是支持反序列化的,即不用在单例类里加上readResolve方法

枚举实现单例 enum EnumAnimal{ INSTANCE; private EnumAnimal() { System.out.println("animal single"); } }

这样就实现了一个动物类单例,它能保证在反序列化后也是单例的,并且是线程安全的,而且也能保证不被反射破坏,具体可以去看反射newInstance()的源码,讲的很清楚。这种方式是不可以实现延迟加载的。

class User{ } enum EnumSingleClass{ INSTANCE; private User user = null; private EnumSingleClass() { System.out.println("EnumSingleClass"); user = new User(); } public User getInstance() { return user; } }

这里用枚举类EnumSingleClass来实现User类的单例。当反序列化User时,发现单例被破坏了,这是毫无疑问的,又不是创建EnumSingleClass的单例,而是创建User的单例,要想反序列化后User单例不被破坏,只能在User中添加readResolve方法。

转载请注明原文地址: https://www.6miu.com/read-1150321.html

最新回复(0)