彻底玩转单例模式

Posted Baret-H

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了彻底玩转单例模式相关的知识,希望对你有一定的参考价值。

image-20210511101958078

  • 饿汉式:类加载时初始化,不存在并发访问问题,会有资源浪费
  • 懒汉式:延时加载,使用时才实例化对象,存在并发访问问题,资源利用率高
  • 双重检测锁式 :利用sychronized关键字解决了懒汉式并发访问问题,同时为了解决指令重排问题使用了volatile关键字
  • 静态内部类式:兼并并发高效调用和延迟加载的优势
  • 枚举单例:实现简单,枚举本身就是单例模式。由JVM从根本上提供保障!避免通过反射和反序列化的漏洞!但是不能延时加载。

1. 饿汉式

package 单例模式;

//饿汉式
public class Hungry {
    private static Hungry hungry = new Hungry();

    //构造器私有
    private Hungry() {
    }

    public static Hungry getInstance() {
        return hungry;
    }
}

优点:static变量会在类装载时初始化,不涉及多个线程访问该对象的问题,可以省略synchronized关键字

缺点:类初始化时就创建了对象,如果只是加载本类,而不是要调用 getInstance(),甚至永远没有调用,则会造成资源浪费!


2. 懒汉式

package 单例模式;

//懒汉式
public class Lazy {
    private static Lazy lazy;

    private Lazy() {

    }

    public static Lazy getInstance() {
        if (lazy == null)
            lazy = new Lazy();
        return lazy;
    }
}

优点:延迟加载,真正用的时候才实例化对象,提高了资源的利用率

缺点:存在并发访问的问题,以下测试并发访问情况

package 单例模式;

//懒汉式
public class Lazy {
    private static Lazy lazy;

    private Lazy() {
        System.out.println("创建示例");
    }

    public static Lazy getInstance() {
        if (lazy == null)
            lazy = new Lazy();
        return lazy;
    }

    public static void main(String[] args) {
        //10条线程并发访问下
        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                Lazy.getInstance();
            }).start();
        }
    }
}

image-20210411162651485
根据结果,可以看到有5个线程打印了结果,也就说进行了5次初始化,这是非常大的漏洞,出现了并发访问的问题


3. 双重检测锁式

为了解决懒汉式并发访问的问题,加入了sychronized关键字

package 单例模式;

//双重检测锁式
public class DoubleLock {
    private static DoubleLock doubleLock;

    private DoubleLock() {
        System.out.println("创建示例");
    }

    public static DoubleLock getInstance() {
        if (doubleLock == null) {
            synchronized (Lazy.class) {
                if (doubleLock == null)
                    doubleLock = new DoubleLock();
            }
        }
        return doubleLock;
    }

    public static void main(String[] args) {
        //10条线程并发访问下
        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                DoubleLock.getInstance();
            }).start();
        }
    }
}

image-20210411164551307
根据打印结果,解决了并发访问的问题;但是这样仍然会存在问题,因为我们new对象时并不是一个完整的原子性操作,而是分为以下三部:

  1. 分配内存空间
  2. 执行构造方法,初始化对象
  3. 把这个对象指向这个空间

单个线程A执行的情况下可以123按顺序执行,也可能由于指令重排按132执行;但是如果线程A按132顺序执行到3时来了一个线程B,此时该对象已经指向了分配的空间,因此B判断对象不是null,就会直接返回对象,但其实对象并没有进行初始化,就造成了错误

因此指令重排也会导致错误,因此完整的双重检测锁式还加入了Volatile关键字来避免指令重排,完整代码如下:

package 单例模式;

//双重检测锁式
public class DoubleLock {
    private volatile static DoubleLock doubleLock;

    private DoubleLock() {
        System.out.println("创建示例");
    }

    public static DoubleLock getInstance() {
        if (doubleLock == null) {
            synchronized (Lazy.class) {
                if (doubleLock == null)
                    doubleLock = new DoubleLock();
            }
        }
        return doubleLock;
    }
}

4. 静态内部类式

package 单例模式;

public class InnerClass {
    private InnerClass() {

    }

    //静态内部类里面创建对象
    public static class inner {
        private static final InnerClass innerClass = new InnerClass();
    }

    public static InnerClass getInstance() {
        return inner.innerClass;
    }
}
  1. 延时加载,只有真正调用getinstance(),才会加载静态内部类。
  2. 线程安全的,Instance是static final类型,保证了内存中只有这样一个实例存在,而且只能被赋值一次,从而保证了线程安全性。
  3. 兼备了并发高效调用和延迟加载的优势

— 反射破坏单例模式,引入枚举单例

以下通过反射对双重检测锁式单例进行破坏

package 单例模式;

import java.lang.reflect.Constructor;

//双重检测锁式
public class DoubleLock {
    private volatile static DoubleLock doubleLock;

    private DoubleLock() {
        System.out.println("创建示例");
    }

    public static DoubleLock getInstance() {
        if (doubleLock == null) {
            synchronized (Lazy.class) {
                if (doubleLock == null)
                    doubleLock = new DoubleLock();
            }
        }
        return doubleLock;
    }

    public static void main(String[] args) throws Exception {
        DoubleLock instance1 = doubleLock.getInstance();
        Constructor<DoubleLock> constructor = DoubleLock.class.getDeclaredConstructor(null);
        constructor.setAccessible(true);
        DoubleLock instance2 = constructor.newInstance();
        System.out.println(instance1);
        System.out.println(instance2);
    }
}

image-20210411233747573
根据结果,看到创建了两个实例,也就是单例模式被破坏,那么怎么解决呢?

可以在私有构造中加锁

package 单例模式;

import java.lang.reflect.Constructor;

//双重检测锁式
public class DoubleLock {
    private volatile static DoubleLock doubleLock;

    private DoubleLock() {
        synchronized (DoubleLock.class){
            if(doubleLock!=null){
                throw new RuntimeException("不要试图使用反射破坏异常");
            }
        }
        System.out.println("创建示例");
    }

    public static DoubleLock getInstance() {
        if (doubleLock == null) {
            synchronized (Lazy.class) {
                if (doubleLock == null)
                    doubleLock = new DoubleLock();
            }
        }
        return doubleLock;
    }

    public static void main(String[] args) throws Exception {
        DoubleLock instance1 = doubleLock.getInstance();
        Constructor<DoubleLock> constructor = DoubleLock.class.getDeclaredConstructor(null);
        constructor.setAccessible(true);
        DoubleLock instance2 = constructor.newInstance();
        System.out.println(instance1);
        System.out.println(instance2);
    }
}

image-20210411234013067
根据结果,可以看到避免了单例模式的破坏?可是上述两个对象一个是通过单例获取,一个通过反射获取;

那如果两个对象都是通过反射获取呢?

public static void main(String[] args) throws Exception {
    Constructor<DoubleLock> constructor = DoubleLock.class.getDeclaredConstructor(null);
    constructor.setAccessible(true);
    DoubleLock instance1= constructor.newInstance();
    DoubleLock instance2 = constructor.newInstance();
    System.out.println(instance1);
    System.out.println(instance2);
}

image-20210411234133836
根据结果,可以看到单例模式又被破坏了,创建了两个对象!这种情况如何解决呢?

可以通过红绿灯方法实现,定义一个标志位记录对象是否创建

package 单例模式;

import java.lang.reflect.Constructor;

//双重检测锁式
public class DoubleLock {
    private volatile static DoubleLock doubleLock;

    //标志位
    private static boolean flag = false;

    private DoubleLock() {
        synchronized (DoubleLock.class) {
            if (flag == false)
                flag = true;
            else
                throw new RuntimeException("不要试图使用反射破坏异常");
        }
        System.out.println("创建示例");
    }

    public static DoubleLock getInstance() {
        if (doubleLock == null) {
            synchronized (Lazy.class) {
                if (doubleLock == null)
                    doubleLock = new DoubleLock();
            }
        }
        return doubleLock;
    }

    public static void main(String[] args) throws Exception {
        Constructor<DoubleLock> constructor = DoubleLock.class.getDeclaredConstructor(null);
        constructor.setAccessible(true);
        DoubleLock instance1 = constructor.newInstance();
        DoubleLock instance2 = constructor.newInstance();
        System.out.println(instance1);
        System.out.println(instance2);
    }
}

image-20210411234412838
可以看到我们通过设置标志位flag再次解决了这个问题,但是一旦被获取了这个关键字,单例模式仍然可以通过反射被破解,如下所示

public static void main(String[] args) throws Exception {
    Constructor<DoubleLock> constructor = DoubleLock.class.getDeclaredConstructor(null);
    Field declaredField = DoubleLock.class.getDeclaredField("flag");
    constructor.setAccessible(true);
    declaredField.setAccessible(true);
    DoubleLock instance1 = constructor.newInstance();
    declaredField.set(instance1, false);//第一个对象创建完毕后将flag改为false
    DoubleLock instance2 = constructor.newInstance();
    System.out.println(instance1);
    System.out.println(instance2);
}

image-20210411234824886
可以看到单例模式再次被破坏;因此为了让程序更加安全,通常对flag关键字进行加密处理

那么到底如何完全的避免反射破坏单例模式呢?我们查看newInstance的源码
image-20210411235142204
可以看到,如果是枚举类型的话,就不能通过反射获取枚举;

因此引入了第5种单例模式


5. 枚举单例

  • 优点:实现简单,枚举本身就是单例模式。由JVM从根本上提供保障!避免通过反射和反序列化的漏洞!

  • 缺点:无延迟加载

package 单例模式;

import java.lang.reflect.Constructor;

//enum本质上就是一个Class类
public enum EnumSingle {
    INSTANCE;

    public static void main(String[] args) throws Exception {
        EnumSingle instance1 = EnumSingle.INSTANCE;
        Constructor<EnumSingle> declaredConstructor = EnumSingle.class.getDeclaredConstructor(null);
        declaredConstructor.setAccessible(true);
        EnumSingle instance2 = declaredConstructor.newInstance();
        System.out.println(instance1);
        System.out.println(instance2);
    }
}

我们再次通过反射创建对象,根据结果报错没有EnumSingle的空构造方法,这不是我们希望看到的

image-20210412000059691
我们对EnumSingle的class文件进行反编译,可以看到明明有空构造方法
image-20210412000353611
但是执行明明报错没有无参构造,我们使用更专业的反编译工具jad对class文件再进行反编译
image-20210412000629008
可以看到枚举类本质上就是继承了Enum类,本身就是一个Class,而且没有无参构造,而是含两个参数的有参构造,我们修改代码在测试

public static void main(String[] args) throws Exception {
    EnumSingle instance1 = EnumSingle.INSTANCE;
    Constructor<EnumSingle> declaredConstructor = EnumSingle.class.getDeclaredConstructor(String.class, int.class);
    declaredConstructor.setAccessible(true);
    EnumSingle instance2 = declaredConstructor.newInstance();
    System.out.println(instance1);
    System.out.println(instance2);
}

image-20210412000837225
这才正确显示了报错的信息:无法反射地创建枚举对象

以上是关于彻底玩转单例模式的主要内容,如果未能解决你的问题,请参考以下文章

彻底玩转单例模式

15彻底玩转单例模式

15彻底玩转单例模式

JUC并发编程(13)--- 彻底玩转单例模式

波吉学设计模式——玩转单例模式

单例模式_反射破坏单例模式_枚举类_枚举类实现单例_枚举类解决单例模式破坏