单例模式

定义:

确保某一个类只有一个实例对象,并且该对象是自行实例化的,通过统一的接口向整个系统提供这个实例对象。

使用场景:

避免产生多个对象消耗过多的资源(比如该对象需要用到IO,Database等等),或者某个类的实例化对象应该只有一个的情况。

因为内存中只有一个实例对象的存在,减少了内存开支,同时,如果该对象的产生需要较多资源的时候(内部需要依赖其他对象...),我们可以采取只生成一个对象,然后让这个对象永久驻留在内存中的方式实现。

如果需要定义大量的静态常量和静态方法,也可以采用单例模式实现。

关键点:

1.构造函数不对外开放,一般为private。

2.通过一个static方法或者枚举返回给外部单例对象。

3.在多线程的条件下也能保证只有一个单例对象。

4.确保单例类对象再反序列化的时候不会创建新的对象。

实现方式:

1.饿汉单例模式

public class Singleton {
    private static Singleton instance = new Singleton();

    private Singleton(){}

    public static Singleton getInstance(){
        return instance;
    }
}

优点:实现简单,在类加载的时候完成了初始化工作,避免了多线程同步问题。

缺点:没有实现懒加载,如果这个单例对象没有被使用过,但是对应的类却加载到内存中的话,也会白白的占用不必要的内存。

2.懒汉单例模式

public class Singleton{
    private static Singleton instance = null;
    
    private Singleton(){}
    
    public static synchronized Singleton getInstance(){
        if(instance == null){
            instance = new Singleton();
        }
        return instance;
    }
}

优点:实现了懒加载,在用到单例对象的时候再对其进行初始化,一定程度上节约了资源。

缺点:getInstance挂了一把锁,每次获取这个单例对象都需要同步,不管是不是并发情况下,都会早成不必要的同步开销。

3.DCL双重检查锁单例模式

懒汉单例模式中,我们并不需要整个getInstance方法都是同步的,我们只需要确保再instance创建的时候,进行同步即可。

public class Singleton{
    private static Singleton instance = null;
    
      private Singleton(){}
    
    public static Singleton getInstance(){
        if(instance == null){
            synchronized(Singleton.class){
                instance = new Singleton();
            }
        }
        return instance;
    }
}

优点:线程安全,懒加载,执行效率高,只有在instance为null的时候才会有同步开销。

缺点:

Double-Checked Lock看起来是非常完美的。但是根据Java的语言规范,上面的代码并非绝对可靠。
出现上述问题, 最重要的2个原因如下:

1, 编译器优化了程序指令, 以加快cpu处理速度.
2, 多核cpu动态调整指令顺序,允许指令乱序执行, 以加快并行运算能力.

问题出现的顺序:

1, 线程A, 发现对象未实例化, 准备开始实例化

2, 由于编译器优化了程序指令, 允许对象在构造函数未调用完前, 将共享变量的引用指向部分构造的对象, 虽然对象未完全实例化, 但已经不为null了.

3, 线程B, 发现部分构造的对象已不是null, 则直接返回了该对象(此时它为null本应该先创建再返回却直接返回了)。

通俗来说,如果线程A的指令发现instance为null,则会去执行初始化的指令,初始化指令最终翻译成汇编指令可能是如下三个部分:

①为内存对象分配内存

②构造函数初始化成员字段

③将创建的对象指定到分配的内存空间中

如果123顺序执行是没有问题的,但是可能存在132乱序执行的情况,如果3执行完成,CPU切换到了另一个线程,同样执行getInstance方法去获取单例对象,单例对象不为空,但是获取到的对象确实不正确的。

这就是DCL失效问题。

改进的办法是,为instance加上volatile修饰符,保证对其修改其它线程立即可见。

private volatile static Singleton instance = null;

虽然volatile又需要额外的性能开销,但是相比安全性,这个开销是值得的。

静态内部类单例模式

public class Singleton{
    private Singleton(){}
    
    public static Singleton getInstance(){
        return SingletonHolder.sInstance;
    }
    
    private static class SingletonHolder{
        private static final Singleton sInstance = new Singleton();
    }
}

根据类加载机制,对于内部类而言,只有再需要的时候才会加载,也就是说位于SingletonHolder中的sInstance只有在第一次调用到getInstance的时候,才会被创建,从而既实现了懒加载,也能够确保线程安全(由JVM确保,在类加载的时候,只有一个线程会执行类加载动作,也就是创建单例对象只会由一个线程完成),推荐使用。

枚举单例

public class EnumSingleton{
    private EnumSingleton(){}
    public static EnumSingleton getInstance(){
        return Singleton.INSTANCE.getInstance();
    }
    
    private static enum Singleton{
        INSTANCE;
        private EnumSingleton singleton;
        // 在加载的时候进行初始化,JVM保证该方法只会被调用一次。
        private Singleton(){
            singleton = new EnumSingleton();
        }
        public EnumSingleton getInstance(){
            return singleton;
        }
    }
}

枚举类和普通类是一样的,但是不同的是枚举实例的创建默认是线程安全的,并且在任何情况下都是只有一个实例对象存在,即便是序列化反序列化也是。

单例模式对(反)序列化的改进

上面所有的单例模式,除了借助枚举来实现外,都存在一个缺点,也就是第四个关键点,我们需要保证单例对象在序列化和反序列化中可以保证对象的一致性,也就是不能通过反序列化违反单例的系统中只存在一个唯一对象的规定。

当然,这个情况的前提是,我们的单例类实现了序列化接口。

通过类的readResolve函数,开发人员可以控制反序列化过程,杜绝在反序列化的时候生成新对象:

public final class Singleton implements Serializable{
    private static final long serialVersionUID = 0L;
    private static final Singleton INSTANCE = new Singleton();
    
    private Singleton(){}
    
    public static Singleton getInstance(){
        return INSTANCE;
    }
    
    private Object readResolve(){
        return INSTANCE;
    }
}

同样的,该方法因为需要用到序列化,自然是要符合序列化的要求,即内部字段也是要可序列化的。

我们将serialVersionUID置为fianl,是为了保证在修改了单例类的内部情况的时候,反序列化也不会抛出InvalidClassException异常,只会将新修改的字段置为默认值。

单例模式的缺点:

优点在开头已经说明了,单例模式的缺点在于它一般没有接口,扩展困难,基本上修改源代码是扩展单例模式的唯一方法。再有,如果单例对象持有Context,很容易引发内存泄露问题,所以一般是用ApplicationContext。


一天八升水
4 声望1 粉丝

计算机本科在读