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

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方法。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值