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

Posted 小样5411

tags:

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

前言

本文将一步步带你实现一个安全的单例(枚举)

单例模式

1、饿汉式单例

饿汉式就是一上来就加载,一上来就new这个类对象,对应内存空间也会一上来就分配,但是没有被使用,这样就浪费了空间,所以不用饿汉式

//饿汉式单例
public class Hungry {

    //假设下面为4组内存资源,但一上来就加载,并没有使用,就浪费了空间
    private byte[] data1 = new byte[1024*1024];
    private byte[] data2 = new byte[1024*1024];
    private byte[] data3 = new byte[1024*1024];
    private byte[] data4 = new byte[1024*1024];

    private Hungry(){

    }

    private final static Hungry hungry = new Hungry();

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

2、懒汉式单例

懒汉式就是等需要用的时候再加载,用的时候调用getInstance

//懒汉式单例
public class LazyMan {

    ///构造器私有
    private LazyMan(){

    }

    private static LazyMan lazyMan;

    public static LazyMan getInstance(){
        if (lazyMan==null){
            lazyMan = new LazyMan();
        }
        return lazyMan;
    }

}

但上述单例在单线程下面是没问题的,但是多线程呢?多线程就有问题了,具体来看看并发下有什么问题

//懒汉式单例
public class LazyMan {

    ///构造器私有
    private LazyMan(){
        System.out.println(Thread.currentThread().getName()+"ok");
    }

    private static LazyMan lazyMan;

    public static LazyMan getInstance(){
        if (lazyMan==null){
            lazyMan = new LazyMan();
        }
        return lazyMan;
    }

    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
            new Thread(()->{
                LazyMan.getInstance();
            }).start();
        }
    }

}

执行了三次,每次结果都不一样,而且不是单例的。这表明线程不安全
在这里插入图片描述

所以要保证线程安全就要上锁,也就出现了双重检测锁(DCL)

//懒汉式单例---双重检测锁
public class LazyMan {

    ///构造器私有
    private LazyMan(){
        System.out.println(Thread.currentThread().getName()+"ok");
    }

    private static LazyMan lazyMan;

    public static LazyMan getInstance(){
        if (lazyMan==null){
            synchronized (LazyMan.class){
                if (lazyMan==null){
                    lazyMan = new LazyMan();
                }
            }
        }
        return lazyMan;
    }

    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
            new Thread(()->{
                LazyMan.getInstance();
            }).start();
        }
    }

}

在这里插入图片描述
这样就保证了单例,但是又出现一个问题,就是new LazyMan()底层不是原子性操作,它的操作分三步:1、分配内存空间 2、执行构造方法、初始化对象 3、将对象指向这个空间

由于执行时计算机会指令重排,所以可能不是按照123顺序执行,而是按照132顺序执行,这样的话如果有两个线程,线程A执行132,线程B再执行的时候由于对象指向了空间lazyMan非空,线程B就以为lazyMan不为空,直接return lazyMan,那么这就有问题了,这里return的lazyMan是没有new的,所以没有分配空间,是空的。所以我们要禁止指令重排,即加volatile关键字
在这里插入图片描述
双重检测锁+原子性操作(volatile)

3、静态内部类实现单例

//静态内部类实现
public class Holder {

    private Holder(){
        System.out.println(Thread.currentThread().getName()+"ok");
    }

    public static Holder getInstance(){
        return InnerClass.holder;
    }

    public static class InnerClass{
        private final static Holder holder = new Holder();
    }

    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
            new Thread(()->{
                Holder.getInstance();
            }).start();
        }
    }
}

这里可以不通过外部类调用,用内部类调用,这也可以实现单例
在这里插入图片描述
但是上面虽然保证线程安全,并且禁止了指令重排可能带来的危险,但是还是能破解,变得不安全,那就是用反射!!!

我们可以用反射破坏一下双重检测锁

import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;

//懒汉式单例
public class LazyMan {

    ///构造器私有
    private LazyMan(){
        System.out.println(Thread.currentThread().getName()+"ok");
    }

    private volatile static LazyMan lazyMan;

    public static LazyMan getInstance(){
        if (lazyMan==null){
            synchronized (LazyMan.class){
                if (lazyMan==null){
                    lazyMan = new LazyMan();
                }
            }
        }
        return lazyMan;
    }

    public static void main(String[] args) throws Exception {
        LazyMan lazyMan = LazyMan.getInstance();
        Constructor<LazyMan> declaredConstructor = LazyMan.class.getDeclaredConstructor(null);//反射获取空参构造
        declaredConstructor.setAccessible(true);//暴力获取私有的空参构造
        LazyMan lazyMan1 = declaredConstructor.newInstance();

        System.out.println(lazyMan);
        System.out.println(lazyMan1);
    }

}

在这里插入图片描述
发现两个就不一样了,那么反射就破坏了单例,但如何解决呢,也很好解决,只要在私有化构造再加一个锁,并判断是否lazy为空

package com.yx.test;

import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;

//懒汉式单例
public class LazyMan {

    ///构造器私有
    private LazyMan(){
        synchronized (LazyMan.class){
            if (lazyMan!=null){
                throw new RuntimeException("不要试图使用反射破坏");
            }
        }
    }

    private volatile static LazyMan lazyMan;

    public static LazyMan getInstance(){
        if (lazyMan==null){
            synchronized (LazyMan.class){
                if (lazyMan==null){
                    lazyMan = new LazyMan();
                }
            }
        }
        return lazyMan;
    }

    public static void main(String[] args) throws Exception {
        LazyMan lazyMan = LazyMan.getInstance();
        Constructor<LazyMan> declaredConstructor = LazyMan.class.getDeclaredConstructor(null);//反射获取空参构造
        declaredConstructor.setAccessible(true);//暴力获取私有的空参构造
        LazyMan lazyMan1 = declaredConstructor.newInstance();

        System.out.println(lazyMan);
        System.out.println(lazyMan1);
    }

}

在这里插入图片描述
但又出现问题,如果两个都用反射创建呢,根本就不通过构造

import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;

//懒汉式单例
public class LazyMan {

    ///构造器私有
    private LazyMan(){
        synchronized (LazyMan.class){
            if (lazyMan!=null){
                throw new RuntimeException("不要试图使用反射破坏");
            }
        }
    }

    private volatile static LazyMan lazyMan;

    public static LazyMan getInstance(){
        if (lazyMan==null){
            synchronized (LazyMan.class){
                if (lazyMan==null){
                    lazyMan = new LazyMan();
                }
            }
        }
        return lazyMan;
    }

    public static void main(String[] args) throws Exception {
        //LazyMan lazyMan = LazyMan.getInstance();
        Constructor<LazyMan> declaredConstructor = LazyMan.class.getDeclaredConstructor(null);//反射获取空参构造
        declaredConstructor.setAccessible(true);//暴力获取私有的空参构造
        LazyMan lazyMan1 = declaredConstructor.newInstance();
        LazyMan lazyMan2 = declaredConstructor.newInstance();

        System.out.println(lazyMan1);
        System.out.println(lazyMan2);
    }

}

在这里插入图片描述
又不安全了,单例被破坏了,那可以又相出一些判断方法进行解决,但是在反射面前都能破解,因为通过反射可以创建对象,获取类的方法和变量,并且对他们进行操作。

有没有什么方法能保证安全不被反射破坏呢?
有,枚举!!!

package com.yx.test;

import java.lang.reflect.Constructor;

public enum  EnumSingle {
    INSTANCE;

    private EnumSingle(){

    }

    public EnumSingle getInstance(){
        return INSTANCE;
    }
}
class Test{
    public static void main(String[] args) throws Exception {
        EnumSingle instance = EnumSingle.INSTANCE;
        EnumSingle instance1 = EnumSingle.INSTANCE;
        System.out.println(instance.hashCode());
        System.out.println(instance1.hashCode());

        Constructor<EnumSingle> declaredConstructor = EnumSingle.class.getDeclaredConstructor(null);//反射获取空参构造
        declaredConstructor.setAccessible(true);//暴力获取私有的空参构造
        EnumSingle lazyMan1 = declaredConstructor.newInstance();
        EnumSingle lazyMan2 = declaredConstructor.newInstance();
        System.out.println(lazyMan1);
        System.out.println(lazyMan2);
    }
}

当我们获取创建的单例并用反射破坏时,会爆出异常
在这里插入图片描述
查看源码会发现,爆出的异常和源码中不一致,按道理应该爆不能用反射创建枚举
在这里插入图片描述
在这里插入图片描述
用jad.exe反编译EnumSingle.class
在这里插入图片描述
反编译生成对应的java文件
在这里插入图片描述

反编译后发现,是有参数的,并且可以看到枚举其实也是class类,只是继承了Enum类
在这里插入图片描述
再次运行,发现确实通过反射会爆出Cannot reflectively create enum objects

在这里插入图片描述
总结:保证单例安全就用枚举,枚举,枚举!!!

视频教程:https://www.bilibili.com/video/BV1B7411L7tE?p=33

以上是关于JUC并发编程(13)--- 彻底玩转单例模式的主要内容,如果未能解决你的问题,请参考以下文章

彻底玩转单例模式

彻底玩转单例模式

15彻底玩转单例模式

15彻底玩转单例模式

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

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