• JUC并发编程学习笔记(十七)彻底玩转单例模式


    彻底玩转单例模式

    单例中最重要的思想------->构造器私有!

    恶汉式、懒汉式(DCL懒汉式!)

    恶汉式

    package single;
    //饿汉式单例(问题:因为一上来就把对象加载了,所以可能会导致浪费内存)
    public class Hungry {
    /*
    * 如果其中有大量的需要开辟的空间,如new byte[1024*1024]这些,那么一开始就会加载,而不是需要时才加载,所以非常浪费空间
    *
    * */
    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();
    public static Hungry getInstance(){
    return HUNGRY;
    }
    }

    懒汉式

    DCL懒汉式

    完整的双重检测锁模式的单例、懒汉式、DCL懒汉式

    package single;
    public class LazyMan {
    private LazyMan() {
    System.out.println(Thread.currentThread() + "ok");
    }
    private volatile static LazyMan lazyMan;
    // 单线程下确实ok
    public static LazyMan getInstance() {
    // 加锁、锁整个类
    // 双重检测锁模式的单例、懒汉式、DCL懒汉式
    if (lazyMan==null){
    synchronized (LazyMan.class){
    if (lazyMan == null) {
    lazyMan = new LazyMan();//不是原子性操作
    }
    }
    }
    return lazyMan;
    }
    /*
    * 1、分配内存空间
    * 2、执行构造方法,初始化对象
    * 3、把这个对象指向这个空间
    *
    * 期望的结果:1、2、3
    * 但是由于指令重排可能导致结果为1、3、2,这在cpu中是没问题的
    * 线程A:1、3、2
    * 线程B如果在线程A执行到3时开始执行判断是否为null,由于已经占用空间了,所以会被判断为不为空,但实际还未初始化对象,实际结果还是为null
    *
    *
    * */
    // 多线程并发测试
    public static void main(String[] args) {
    for (int i = 0; i < 10; i++) {
    new Thread(() -> {
    LazyMan.getInstance();
    }).start();
    }
    }
    }

    但是有反射!只要有反射,任何的代码都不安全,任何的私有关键字都是摆设

    正常的单例模式:

    /*
    * 正常的单例模式创建的都为同一个对象,并且该对象全局唯一
    * 只执行一次创建,并且对象都是同一个
    * Thread[main,5,main]ok
    * true
    * */
    LazyMan instance1 = LazyMan.getInstance();
    LazyMan instance2 = LazyMan.getInstance();
    System.out.println(instance2==instance1);

    反射破坏单例:

    /*
    * 通过反射破坏单例
    * 执行两个创建,两个不同的对象
    * Thread[main,5,main]ok
    Thread[main,5,main]ok
    false
    * */
    LazyMan instance1 = LazyMan.getInstance();
    Constructor declaredConstructor = LazyMan.class.getDeclaredConstructor();
    declaredConstructor.setAccessible(true);
    LazyMan instance2 = declaredConstructor.newInstance();
    System.out.println(instance2 == instance1);

    怎么去解决这种破坏呢?

    首先反射走了无参构造器,我们可以在构造器中进行加锁判断是否已经存在了对象。

    private LazyMan() {
    //通过构造器来加锁判断防止反射破坏
    synchronized (LazyMan.class){
    if (lazyMan!=null){
    throw new RuntimeException("不要试图使用反射破坏单例模式");
    }
    }
    }

    通过反射破坏单例模式

    道高一尺,魔高一丈

    1、通过普通的反射来破坏单例模式

    Constructor declaredConstructor = LazyMan.class.getDeclaredConstructor();
    declaredConstructor.setAccessible(true);
    LazyMan lazyMan1 = LazyMan.getInstance();
    LazyMan lazyMan2 = declaredConstructor.newInstance();
    System.out.println(lazyMan1);
    System.out.println(lazyMan2);

    解决方法:通过构造器加锁解决

    private LazyMan() {
    //通过构造器来加锁判断防止反射破坏
    synchronized (LazyMan.class){
    if (lazyMan == null){
    }else {
    throw new RuntimeException("不要试图使用反射破坏单例模式");
    }
    }
    }

    2、通过反射创建两个类来破坏单例模式

    Constructor declaredConstructor = LazyMan.class.getDeclaredConstructor();
    declaredConstructor.setAccessible(true);
    LazyMan lazyMan1 = declaredConstructor.newInstance();
    LazyMan lazyMan2 = declaredConstructor.newInstance();
    System.out.println(lazyMan1);
    System.out.println(lazyMan2);

    解决方法:设置一个外部私有变量,在构造方法中通过外部私有变量来操作

    //创建一个外部的标,用于防止通过newInstance破坏单例模式
    private static boolean flg = true;
    private LazyMan() {
    //通过构造器来加锁判断防止反射破坏
    synchronized (LazyMan.class){
    if (flg){
    flg = false;
    }else {
    throw new RuntimeException("不要试图使用反射破坏单例模式");
    }
    }
    }

    3、通过反射字段来将外部私有变量修改。

    Constructor declaredConstructor = LazyMan.class.getDeclaredConstructor();
    declaredConstructor.setAccessible(true);
    //通过反射修改内部私有变量
    Field flg1 = LazyMan.class.getDeclaredField("flg");
    flg1.setAccessible(true);
    //通过反射的newInstance创建的两个对象依旧破坏了单例模式
    LazyMan instance1 = declaredConstructor.newInstance();
    //通过反射字段对单例模式进行破坏
    flg1.set(instance1,true);
    LazyMan instance2 = declaredConstructor.newInstance();
    System.out.println(instance2 == instance1);

    解决方法,通过枚举类型!枚举类型自带单例模式,禁止反射破坏

    package single;
    import java.lang.reflect.Constructor;
    import java.lang.reflect.InvocationTargetException;
    //枚举类
    public enum EnumDemo {
    INSTANCE;
    public EnumDemo getInstance(){
    return INSTANCE;
    }
    }
    class EnumDemoTest{
    public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
    Constructor declaredConstructor = EnumDemo.class.getDeclaredConstructor(null);
    declaredConstructor.setAccessible(true);
    EnumDemo enumDemo1 = declaredConstructor.newInstance();
    EnumDemo enumDemo2 = declaredConstructor.newInstance();
    System.out.println(enumDemo1);
    System.out.println(enumDemo2);
    }
    }

    发现抱错,没有对应的无参构造

    但是idea编译的源码中是由无参构造的

    idea欺骗了我们,那么编译好的类到底有没有无参构造,通过javap -p反编译源码查看所以方法

    可以看到,也有空参的构造方法,也就意味了反编译源码也欺骗了你,所以我们通过更专业的工具来查看,使用jad查看。

    查看当前目录新生成的java文件可以发现,通过jad反编译的源码的构造函数时个有参构造函数

    // Decompiled by Jad v1.5.8g. Copyright 2001 Pavel Kouznetsov.
    // Jad home page: http://www.kpdus.com/jad.html
    // Decompiler options: packimports(3)
    // Source File Name: EnumDemo.java
    package single;
    public final class EnumDemo extends Enum
    {
    public static EnumDemo[] values()
    {
    return (EnumDemo[])$VALUES.clone();
    }
    public static EnumDemo valueOf(String name)
    {
    return (EnumDemo)Enum.valueOf(single/EnumDemo, name);
    }
    private EnumDemo(String s, int i)
    {
    super(s, i);
    }
    public EnumDemo getInstance()
    {
    return INSTANCE;
    }
    public static final EnumDemo INSTANCE;
    private static final EnumDemo $VALUES[];
    static
    {
    INSTANCE = new EnumDemo("INSTANCE", 0);
    $VALUES = (new EnumDemo[] {
    INSTANCE
    });
    }
    }

    我们尝试在反射中加入这两个参数类

    Constructor declaredConstructor = EnumDemo.class.getDeclaredConstructor(String.class,int.class);

    可以发现,它根据我们预想的结果抛出一个异常

    在newInstance方法中如果时枚举类就会抛出这个异常,这是从反射层面限制了对枚举类单例模式的破坏!!

  • 相关阅读:
    LeetCode C++ 67.二进制求和
    【Qt】QGroundControl入门1:介绍
    猿辅导创新教育研究院解读新课标:三大变化要注意
    MySQL关于日期函数的使用-笔记
    CP Autosar中的PNC说明
    JavaFX笔记
    【博客498】k8s kubelet device-plugins
    Dart笔记:stream_channel 包用法
    我的2023
    SwiftUI 4.0 中原生图表(Charts)实现超长内容滚动功能
  • 原文地址:https://www.cnblogs.com/nhgtx/p/17806041.html