设计模式-单例模式(附改进流程)

设计模式-单例模式

4.24 修改一些对静态内部类的解释

在日常学习中,经常会碰到单例模式,所以在这里系统的记录一下单例模式。

首先,让我们看一下它的定义,Java中对单例模式的定义是:

一个类有且仅有一个实例,并且自行实例化向整个系统提供

所以我们需要实现的就是如何保证一个类有且仅有一个实例。

实现过程

为了不让其他类来创建这个对象,我们首先要做到的就是构造器私有。接着,我们直接在类中定义一个对象,即可实现唯一

public class Singleton{
    //构造器私有
    private Singleton(){
    }
    
    private final static Singleton singleton = new Singleton();
    
    //让其他类获得
    public static Singleton getInstance(){
        return singleton;
    }
}

这种创建实例的方法就是我们常见的饿汉式单例。其名字是由the singleton instance is early created at complie time中的early音译过来的。

显而易见,饿汉式单例在类加载时会将类中方法,属性直接加载完成。当我们还未使用它时,它便自动加载了,这样难免会造成内存的消耗

为了解决上诉问题,所以就有了第二种的单例类型:懒汉式单例。其名字是由the singleton instance is lazily created at runtime中的lazily意译过来的。

让我们看下是如何创建的。

public class Singleton{
    //构造器私有
    private Singleton(){
    }
    
    private static Singleton singleton;
   
    //当需要使用的时候才加载 符合名字
    public static Singleton getInstance(){
        if (singleton == null){
            singleton = new Singleton();
        }
        return singleton;
    }
}

当我们使用以上代码时,在一定条件下,确实可以保证单例。这个条件便是单线程!该方法在并发下,便不能保证单例,所以我们要对该方法做一定的改进。
讲到并发,我们第一个想到的肯定就是锁,那么我们开始进行第一次改造:

    private static Singleton singleton;

    //将synchronize加到类上
    public synchronized static Singleton getInstance(){
        if (singleton == null){
            singleton = new Singleton();
        }
        return singleton;
    }

将方法加到类上,是可行的,但是锁的粒度太大了,会影响整个效率,那么我们减小锁的粒度,对其进行第二次改造:


    private static Singleton singleton;

   
    public static Singleton getInstance(){
        //给对象进行加锁 
        //判断两次是为了,两个对象可能在第一次判空后才开始进行锁竞争 若加在最外层 则与上一种情况类似
        if (singleton == null) {
            synchronized (Singleton.class) {
                if (singleton == null) {
                    singleton = new Singleton();
                }
            }
        }
        return singleton;

利用双重检测来保证了线程上的安全,这个就是懒汉-DCL的模式。从线程的角度来讲,现在已经能够保证只取到一个对象了。但是由于singleton = new Singleton()这行代码的原因,还会产生一种错误:线程会拿到还未初始化完成的对象。因为这句代码不具备原子性,在JVM层面上,会进行指令上的重排序。为了避免指令上的重排序,所以我们要用volatile进行带三次改造。

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

至此,我们好像得到了很优秀的一种单例模式。但是这些就是完美的嘛?不是的,还有反射这个拦路虎。
我们对进行如下尝试:

        Singleton singleton1 = Singleton.getInstance();
        Constructor constructor = Singleton.class.getDeclaredConstructor();
        constructor.setAccessible(true);
        Singleton singleton2 = constructor.newInstance();

        System.out.println(singleton1);
        System.out.println(singleton2);

发现输出了:
image.png

很明显,单例模式被破解了。那么我们还需继续进行改进,在构造器上再加上一步判断

 private Singleton() {
        synchronized(Singleton.class){
            if (singleton != null){
                throw new RuntimeException();
            }
        }
    }

上述问题被解决了。

image.png

但是若我们都用反射创建对象会如何呢?

    Constructor constructor = Singleton.class.getDeclaredConstructor();
    constructor.setAccessible(true);
        
    Singleton singleton1 = constructor.newInstance();
    Singleton singleton2 = constructor.newInstance();
        
    System.out.println(singleton1);
    System.out.println(singleton2);

image.png

依然出现了问题。此时我们需要利用一个变量来记录,当第一次被调用时,改变状态,当第二次被调用时直接返回异常。

    private static boolean jathow = false;
    
    private Singleton() {
        synchronized(Singleton.class){
            if (jathow == false){
                jathow = true;
            }else {
                throw new RuntimeException("禁止反射");
            }
        }
    }

但是对于上述改进,仍有破解办法。

    Field jathow = Singleton.class.getDeclaredField("jathow");
    jathow.setAccessible(true);

    Constructor constructor = Singleton.class.getDeclaredConstructor();
    constructor.setAccessible(true);

    Singleton singleton1 = constructor.newInstance();
    jathow.set(singleton1, false);
    Singleton singleton2 = constructor.newInstance();
        
    System.out.println(singleton1);
    System.out.println(singleton2);

我们对可以对属性进行加密,增大破解难度,但是没有从根源上解决这个问题。

我们只能从反射的源码进行分析,看看有什么办法可以从根源上解决这个问题。查看源码我们可以看到
image.png

所以枚举类单例就是我们最后的救星了。

public enum EnumSingleton {

    INSTANCE;

    public EnumSingleton getInstance(){
        return INSTANCE;
    }

}

至此算是解决了我目前这个层面能想到的问题。

还有一种算是饿汉模式升级版的静态内部类。
由于是内部类 所以在外面类进行加载时 不会第一时间加载类内部的实例,也就实现了延迟加载。
那么它是如何保证线程安全的呢?
由于JVM的类加载机制存在的 指令,该指令在加载类时,会阻塞其他的线程,这样就会保证了单例。

那么DCL与它的差异在哪里呢?
差异在于 DCL模式可以自己传入参数 ,对于实例对象做一点小小的定制。但是静态内部类加载则不行了,所以从这个角度来看不够灵活。
具体在工作中如何使用就看个人选择了。

  • 静态内部类
public class Singleton {
    private Singleton() {

    }

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

你可能感兴趣的:(反射,java,单例模式)