15彻底玩转单例模式

Posted zxhbk

tags:

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

引用学习(狂神说)

饿汉式 DCL懒汉式,深究!

饿汉式创建单例

饿汉式:顾名思义很饿:在类加载的时候,直接初始化对象

  • 缺点:很浪费资源,因为对象没有被使用,但是已经初始化在内存了

    • 比如:有下面这样的数组,会很浪费资源

package com.zxh.single;

/**
 *  饿汉式:顾名思义很饿
 *  1、在类加载的时候,直接初始化对象
 *  2、很浪费资源,因为对象没有被使用,但是已经初始化在内存了
 *      比如:有下面这样的数组,会很浪费资源
 */
public class Hungry {
    private byte[] buffer1 = new byte[1024*1024];
    private byte[] buffer2 = new byte[1024*1024];
    private byte[] buffer3 = new byte[1024*1024];
    private byte[] buffer4 = new byte[1024*1024];

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

    }
    // 直接初始化对象
    private final static Hungry HUNGRY = new Hungry();

    public static Hungry getInstance(){
        return HUNGRY;
    }

    public static void main(String[] args) {
        Hungry hungry1 = Hungry.getInstance();
        Hungry hungry2 = Hungry.getInstance();
        System.out.println(hungry1);
        System.out.println(hungry2);
    }
}

技术图片

 浪费资源所以就有了懒汉式创建单例模式

懒汉式创建单例

普通的懒汉式

package com.zxh.single;

/**
 * 懒汉式创建单例模式
 */
public class LazyMan {

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

    }

    private static LazyMan LAZY_MAN;

    public static LazyMan getInstance(){
        if(LAZY_MAN == null){   // 如果为空,初始化对象
            LAZY_MAN = new LazyMan();
        }
        return LAZY_MAN;    // 并返回
    }

    public static void main(String[] args) {
        LazyMan lazyMan1 = LazyMan.getInstance();
        LazyMan lazyMan2 = LazyMan.getInstance();
        System.out.println(lazyMan1);
        System.out.println(lazyMan2);
    }

}

技术图片

多线程破坏普通的懒汉式

package com.zxh.single;

/**
 * 懒汉式创建单例模式
 */
public class LazyMan {

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

    private static LazyMan LAZY_MAN;

    public static LazyMan getInstance(){
        if(LAZY_MAN == null){   // 如果为空,初始化对象
            LAZY_MAN = new LazyMan();
        }
        return LAZY_MAN;    // 并返回
    }

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

}

技术图片

可以看到经过了4次构造方法,也就是创建了4个不同的对象。

那么如何解决多线程的并发问题?使用DCL (双重检测锁)懒汉式

DCL 懒汉式创建

  • DCL就是双重检测锁:解决并发问题

增加同步代码块:解决并发问题

  • 修改getInstance这个方法

public static LazyMan getInstance(){
    synchronized (LazyMan.class){   // 直接锁class模板
        if(LAZY_MAN == null){   // 如果为空,初始化对象
            LAZY_MAN = new LazyMan();
        }
    }
    return LAZY_MAN;    // 并返回
}

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

技术图片

缺点:影响效率,因为每个线程都需要同步等待。

解决:在外面增加判断如果对象已经创建,那么直接返回

增加判断:解决效率问题

  • 解决同步的效率问题

// DCL 双重检测锁
public static LazyMan getInstance(){
    if (LAZY_MAN == null) { // 第一重检测
        synchronized (LazyMan.class){   // 直接锁class模板,锁
            if(LAZY_MAN == null){   // 如果为空,初始化对象,第二重检测
                LAZY_MAN = new LazyMan();
            }
        }
    }
    return LAZY_MAN;    // 并返回
}

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

技术图片

volatile:解决指令重排

存在问题:指令重排

// DCL 双重检测锁
public static LazyMan getInstance(){
    if (LAZY_MAN == null) { // 第一重检测
        synchronized (LazyMan.class){   // 直接锁class模板,锁
            if(LAZY_MAN == null){   // 如果为空,初始化对象,第二重检测
               /**
                 * 但是真的安全吗?不安全
                 * 因为初始化对象不是原子操作,在极端情况下会进行指令重排
                 * 初始化对象的时候,不要以为只有一行代码,但是执行的时候会分成3步
                 * 1、分配内存空间
                 * 2、执行构造方法,初始化对象
                 * 3、把这个对象指向这个空间
                 *
                 * 比如:我们希望执行的顺序为 123,
                 * 假如A线程进入,经过指令重排执行顺序为 132,当执行到13,还没有执行2的时候,内存空间是分配了也指向了这个空间,但是对象是空的
                 * 此时B线程进入了,发现对象已经分配了空间,直接返回了,就会造成空指针
                 */
                LAZY_MAN = new LazyMan();
            }
        }
    }
    return LAZY_MAN;    // 并返回
}

解决问题

  • 增加volatile关键字,防止初始化对象的时候,计算机指令重排

  • private volatile static LazyMan LAZY_MAN;

/**
 * 懒汉式创建单例模式
 */
public class LazyMan {

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

    private volatile static LazyMan LAZY_MAN;

    // DCL 双重检测锁
    public static LazyMan getInstance(){
        if (LAZY_MAN == null) { // 第一重检测
            synchronized (LazyMan.class){   // 直接锁class模板,锁
                if(LAZY_MAN == null){   // 如果为空,初始化对象,第二重检测
                    LAZY_MAN = new LazyMan();
                }
            }
        }
        return LAZY_MAN;    // 并返回
    }

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

}

静态内部类创建

package com.zxh.single;

// 静态内部类创建
public class Holder {

    private Holder(){

    }

    public static InnerClass getInstance(){
        return InnerClass.INNER_CLASS;
    }

    // 静态内部类,在程序加载时,并不会被初始化,所以不会浪费资源
    private static class InnerClass{
        private final static InnerClass INNER_CLASS = new InnerClass();
    }

    public static void main(String[] args) {
        InnerClass innerClass1 = Holder.getInstance();
        InnerClass innerClass2 = Holder.getInstance();

        System.out.println(innerClass1);
        System.out.println(innerClass2);
    }

}

技术图片

反射破坏DCL和防止破坏

  • 现在DCL 是目前我们认为最厉害的

  • 但是在反射面前,一切都时候浮云

反射创建对象

1、创建两个对象:第一个为普通创建,第二个使用反射创建

  • 会创建两个不同的对象

package com.zxh.single;

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

/**
 * 懒汉式创建单例模式
 */
public class LazyMan {

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

    private volatile static LazyMan LAZY_MAN;

    // DCL 双重检测锁
    public static LazyMan getInstance(){
        if (LAZY_MAN == null) { // 第一重检测
            synchronized (LazyMan.class){   // 直接锁class模板,锁
                if(LAZY_MAN == null){   // 如果为空,初始化对象,第二重检测

                    LAZY_MAN = new LazyMan();
                }
            }
        }
        return LAZY_MAN;    // 并返回
    }

    // NoSuchMethodException:没有这个方法
    public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
        LazyMan lazyMan1 = LazyMan.getInstance();
        Constructor<LazyMan> lazyManConstructor = LazyMan.class.getDeclaredConstructor(null);
        LazyMan lazyMan2 = lazyManConstructor.newInstance(null);

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

}

技术图片

 解决:在构造器中在增加一重判断

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

技术图片

 2、创建两个对象:两个对象都是用反射创建

// NoSuchMethodException:没有这个方法
public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
    Constructor<LazyMan> lazyManConstructor = LazyMan.class.getDeclaredConstructor(null);
    LazyMan lazyMan1 = lazyManConstructor.newInstance(null);
    LazyMan lazyMan2 = lazyManConstructor.newInstance(null);

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

技术图片

 解决:增加一个标志变量,来判断是否是第一次创建对象

  • private static boolean flag = false:注意必须使用static修饰,才能是全局的变量

package com.zxh.single;

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

/**
 * 懒汉式创建单例模式
 */
public class LazyMan {

    private static boolean flag = false;    // 增加一个标志,来判别是否已经创建了对象

    // 构造器私有化
    private LazyMan(){
        synchronized (LazyMan.class){
            if(flag == false){  // 第一次进入
                flag = true;   // 表示已将创建了对象
            }else{
                throw new RuntimeException("不要试图利用反射破坏单例");
            }
        }
    }

    private volatile static LazyMan LAZY_MAN;

    // DCL 双重检测锁
    public static LazyMan getInstance(){
        if (LAZY_MAN == null) { // 第一重检测
            synchronized (LazyMan.class){   // 直接锁class模板,锁
                if(LAZY_MAN == null){   // 如果为空,初始化对象,第二重检测
                    LAZY_MAN = new LazyMan();
                }
            }
        }
        return LAZY_MAN;    // 并返回
    }

    // NoSuchMethodException:没有这个方法
    public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
        Constructor<LazyMan> lazyManConstructor = LazyMan.class.getDeclaredConstructor(null);
          LazyMan lazyMan1 = lazyManConstructor.newInstance(null);
        LazyMan lazyMan2 = lazyManConstructor.newInstance(null);

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

}

技术图片

 3、通过修改字段属性来破坏

// NoSuchMethodException:没有这个方法
public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException, NoSuchFieldException, ClassNotFoundException {
    Constructor<LazyMan> lazyManConstructor = LazyMan.class.getDeclaredConstructor(null);
    LazyMan lazyMan1 = lazyManConstructor.newInstance(null);
    Field flag = LazyMan.class.getDeclaredField("flag");
    flag.setAccessible(true);   // 关闭安全监测锁,提高创建对象的效率
    flag.set(lazyMan1, false);
    LazyMan lazyMan2 = lazyManConstructor.newInstance(null);

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

技术图片

哪怕这个标志的字段是加密的,也有可能会被反编译破解,从而获取字段信息。

所以说魔高一尺,道高一丈!

 

但是我们还可以利用枚举创建,因为它的底层就是不允许通过反射创建对象的

枚举创建单例和分析

枚举的单例创建

package com.zxh.single;

public enum EnumSingle {
    INSTANCE;

    public EnumSingle getInstance(){
        return INSTANCE;
    }

    public static void main(String[] args) {
        EnumSingle instance1 = EnumSingle.INSTANCE;
        EnumSingle instance2 = EnumSingle.INSTANCE;
        System.out.println(instance1 == instance2);
    }

}

技术图片

源码分析反射的newInstance()方法

1、进入newInstance()方法

技术图片

2、发现如果是枚举类,就抛出不能使用反射创建枚举类异常

技术图片

利用反射破坏测试

package com.zxh.single;

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

public enum EnumSingle {
    INSTANCE;

    public EnumSingle getInstance(){
        return INSTANCE;
    }

    public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
        EnumSingle instance1 = EnumSingle.INSTANCE;
        Constructor<EnumSingle> constructor = EnumSingle.class.getDeclaredConstructor(null);
        EnumSingle instance2 = constructor.newInstance(null);

        System.out.println(instance1 == instance2);
    }

}

技术图片

反射调用时,存在问题并解决问题

存在问题

发现抛出异常时没有这个方法,和想象中会抛出的异常不同,为什么?

是没有这个构造器吗?

1、通过编译生成的target包中对应的class文件查看

  • 发现有这个构造器

技术图片

2、通过反编译class文件查看,进入指定的目录在命令行输入:javap -p EnumSingle.class

  • 发现也有这个构造器

技术图片

 难道是idea和jdk骗了我们?

3、使用专业的软件反编译

jad百度网盘下载 提取码: 9fpa

1)进入指定目录输入,jad -sjava EnumSingle.class

  • 需要将该执行文件和class文件放在一起,并且会生成在同一目录下

技术图片

 技术图片

 2)编译得到的文件中可以发现没有空构造,但是有一个有参构造技术图片

 解决问题

  • 利用反射调用这个构造器
package com.zxh.single;

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

public enum EnumSingle {
    INSTANCE;

    public EnumSingle getInstance(){
        return INSTANCE;
    }

    public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
        EnumSingle instance1 = EnumSingle.INSTANCE;
        Constructor<EnumSingle> constructor = EnumSingle.class.getDeclaredConstructor(String.class, int.class);
        EnumSingle instance2 = constructor.newInstance(null);

        System.out.println(instance1 == instance2);
    }

}
技术图片

成功返回对应的异常

总结

枚举类是创建单例模式最安全的,推荐使用!

 

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

彻底玩转单例模式

彻底玩转单例模式

彻底玩转单例模式

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

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

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