厚积薄发打卡Day28 :狂神说Java之彻底玩转单例设计模式

前言:

学习视频来源:【狂神说Java】JUC并发编程最新版通俗易懂

本文承接上篇博客:狂神说Java之从JMM 到 volatile

这节设计模式是在JUC穿插讲解的,但又是十分基础与重要,因此特地单拎出来好好学习。

开整 🚀🚀🚀

18.彻底玩转单例模式

参考文章:
菜鸟教程-单例模式
深入理解设计模式(一):单例模式
彻底玩转单例模式

单例模式:

简介:

单例模式是一种常用的软件设计模式,其定义是单例对象的类只能允许一个实例存在。

注意:

  • 1、单例类只能有一个实例。
  • 2、单例类必须自己创建自己的唯一实例。
  • 3、单例类必须给所有其他对象提供这一实例。

许多时候整个系统只需要拥有一个的全局对象,这样有利于我们协调系统整体的行为。

适用场景:

  • 1.需要生成唯一序列的环境
  • 2.需要频繁实例化然后销毁的对象。
  • 3.创建对象时耗时过多或者耗资源过多,但又经常用到的对象。
  • 4.方便资源相互通信的环境

构建步骤:

  1. 将该类的构造方法定义为私有方法
    • 这样其他处的代码就无法通过调用该类的构造方法来实例化该类的对象,只有通过该类提供的静态方法来得到该类的唯一实例;
  2. 在该类内提供一个静态实例化方法
    • 当我们调用这个方法时,如果类持有的引用不为空就返回这个引用,
    • 如果类保持的引用为空就创建该类的实例并将实例的引用赋予该类保持的引用。

优缺点:

优点

  • 在内存中只有一个对象,节省内存空间;
  • 避免频繁的创建销毁对象,可以提高性能;
  • 避免对共享资源的多重占用,简化访问;
  • 为整个系统提供一个全局访问点。

缺点

  • 不适用于变化频繁的对象;

  • 滥用单例将带来一些负面问题,如为了节省资源将数据库连接池对象设计为的单例类,可能会导致共享连接池对象的程序过多而出现连接池溢出;

  • 如果实例化的对象长时间不被利用,系统会认为该对象是垃圾而被回收,这可能会导致对象状态的丢失;

饿汉式单例模式:

饿汉式:饥饿的人,看到食物就上来抢。对应在代码中,在类加载时就立刻实例化。

public class HungrySingleton {
   
    //假如类中存在这样的空间开辟的操作:
    //使用饿汉式时,不管用不用,上来就给你开了再说,造成空间浪费。
    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];

    //1、私有构造器
    private HungrySingleton(){

    }
    //2、类的内部创建对象
    private final static HungrySingleton HUNGRYSINGLE = new HungrySingleton();

    //3、向外暴露一个静态的公共方法。 getInstance
    public static HungrySingleton getInstance(){
        return HUNGRYSINGLE;
    }
    public static void main(String[] args) {
        //单线程:
        HungrySingleton instance1 = HungrySingleton.getInstance();
        HungrySingleton instance2 = HungrySingleton.getInstance();
        System.out.println(instance1 == instance2);//true
        System.out.println("------------");
        //多线程:
        new Thread(()->{
            HungrySingleton instance_A = HungrySingleton.getInstance();
            System.out.println(instance_A);
            //com.kuangstudy.Singleton.HungrySingleton@626213bf
        }).start();
        new Thread(()->{
            HungrySingleton instance_B = HungrySingleton.getInstance();
            System.out.println(instance_B);
            //com.kuangstudy.Singleton.HungrySingleton@626213bf
        }).start();
    }
}

小结:

  • 优点:
    1. 基于 classloader 机制避免了多线程的同步问题,在类装载的时候就完成实例化。避免了线程同步问题==>线程安全。
    2. 没有加锁,执行效率会提高。
    3. 简单好用。
  • 缺点:
    1. 在类装载的时候就完成实例化,没有达到Lazy Loading的效果。如果从始至终从未使用过这个实例,则会造成内存的浪费。

懒汉式单例模式:

非线程安全:

public class LazySingleton {

    //1.私有化构造函数
    private LazySingleton() {
        System.out.println(Thread.currentThread().getName()+" ->OK");
    }

    //2.创建对象(容器)
    private static LazySingleton lazyMan ;

    //3.对外提供静态实例化方法,判断在对象为空的时候创建
    public static LazySingleton getInstance(){
        //用的时候再加载
        if (lazyMan == null) {
            lazyMan = new LazySingleton();
        }
        return lazyMan;
    }
    //测试单线程下懒汉式单例
    public static void main(String[] args) {
        LazySingleton instance1 = LazySingleton.getInstance();
        LazySingleton instance2 = LazySingleton.getInstance();
        //实例化两个只出现:main ->OK 
        System.out.println(instance1);//com.kuangstudy.Singleton.LazySingleton@61bbe9ba
        System.out.println(instance2);//com.kuangstudy.Singleton.LazySingleton@61bbe9ba
    }
}
小结
  • 这种方式是最基本的实现方式,这种实现最大的问题就是不支持多线程。因为没有加锁 synchronized,所以严格意义上它并不算单例模式。

  • 这种方式 lazy loading 很明显,不要求线程安全,在多线程不能正常工作。

    public class LazySingleton {
    
        //1.私有化构造函数
        private LazySingleton() {
            System.out.println(Thread.currentThread().getName()+" ->OK");
        }
    
        //2.创建对象(容器)
        private static LazySingleton lazyMan ;
    
        //3.对外提供静态实例化方法,判断在对象为空的时候创建
        public static LazySingleton getInstance(){
            //用的时候再加载
            if (lazyMan == null) {
                lazyMan = new LazySingleton();
            }
            return lazyMan;
        }
        //开启多条线程实例化LazySingleton
        public static void main(String[] args) {
            for (int i = 0; i < 100; i++) {
                //            new Thread(()->{LazySingleton.getInstance();}).start();
                new Thread(LazySingleton::getInstance).start();
            }
        }
        //结果实例化超过1个对象:(不唯一)
        /*
    
        Thread-0 ->OK
        Thread-3 ->OK
        Thread-2 ->OK
        Thread-1 ->OK
    
        */
    }
    
    

线程安全:

public class LazySingletonWithDCL {

    //1.私有化构造函数
    private LazySingletonWithDCL() {
        System.out.println(Thread.currentThread().getName() + " ->OK");
    }

    //2.创建对象(容器)
    //    private static LazySingletonWithDCL lazyMan ;
    //5.new 不是一个原子性操作:
    private volatile static LazySingletonWithDCL lazyMan;

    //3.对外提供静态实例化方法
    //4.为保证线程安全,需要上锁
    public static LazySingletonWithDCL getInstance() {
        if (lazyMan == null) {
            synchronized (LazySingletonWithDCL.class) {
                if (lazyMan == null) {
                    lazyMan = new LazySingletonWithDCL();
                }
            }
        }
        return lazyMan;
    }

    //开启多条线程实例化LazySingletonWithDCL
    public static void main(String[] args) {
        for (int i = 0; i < 100; i++) {
            //            new Thread(()->{LazySingletonWithDCL.getInstance();}).start();
            new Thread(LazySingletonWithDCL::getInstance).start();
        }
    }
}
问题:
  1. 为什么要synchronized (LazySingletonWithDCL.class)而不是对方法加锁?

    相关博客:Synchronized方法锁、对象锁、类锁区别 (精)

    简单回答:

    1. synchronized 重量级锁,锁的范围越小越好,class只有一个,而方法会每次都执行,因此为了提高效率,锁对象而不是锁方法。
  2. new对象的过程为什么不是一个原子性操作?

    相关博客:new一个对象竟然不是原子操作?

    实锤 new 对象不是原子性操作,会执行以下操作:

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

    实践出真知:

    1. 这是原子类AtomicInteger.getAndIncrement()方法,反编译后:
      在这里插入图片描述

    2. public class TestNew {
      
          private static int num = 0;
      
          public static void main(String[] args) {
              TestNew testNew = new TestNew();
          }
          public void add(){
              num++;
          };
      }
      

      反编译后:

      在这里插入图片描述

小结:
  • 双检锁/双重校验锁(DCL,即 double-checked locking)
    • 这种方式采用双锁机制,安全且在多线程情况下能保持高性能。getInstance() 的性能对应用程序很关键。
  • 这样,实例化代码只用执行一次,后面再次访问时,判断 if(lazyMan == null),直接return实例化对象,也避免的反复进行方法同步.
  • 线程安全;延迟加载;效率较高

反射破坏单例:

在Java进阶中学习过非常暴力的获取类的方式:反射(详见:注解与反射

因此对于在单例模式中私有的方法,我们可以通过反射进行破解:

  1. 对DCL饿汉式单例进行破解:

实力诠释再多的🔒也抵不住反射的暴力破解

public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
    LazySingletonWithDCL instance = LazySingletonWithDCL.getInstance();
    //利用反射创建对象
    //获取无参构造
    Constructor<LazySingletonWithDCL> declaredConstructor = LazySingletonWithDCL.class.getDeclaredConstructor(null);
    //开放权限
    declaredConstructor.setAccessible(true);
    LazySingletonWithDCL instance_withReflect = declaredConstructor.newInstance();

    System.out.println(instance); //LazySingletonWithDCL@61bbe9ba
    System.out.println(instance_withReflect);  //LazySingletonWithDCL@610455d6
    System.out.println(instance == instance_withReflect);  //false
}
  1. 单例模式增加校验反击:

    • 因为反射走的是无参构造,可以在构造函数中进行判断
public class LazySingletonWithDCL {

    //1.私有化构造函数
    //6.增加对反射的判断
    private LazySingletonWithDCL() {
        synchronized (LazySingletonWithDCL.class){
            //6.1如果此时已经有实例,阻止反射创建
            if (lazyMan != null){
                throw new RuntimeException("不要试图通过反射破解单例");
            }
        }
        System.out.println(Thread.currentThread().getName() + " ->OK");
    }

    //2.创建对象(容器)
    //    private static LazySingletonWithDCL lazyMan ;
    //5.new 不是一个原子性操作:
    private volatile static LazySingletonWithDCL lazyMan;

    //3.对外提供静态实例化方法
    //4.为保证线程安全,需要上锁
    public static LazySingletonWithDCL getInstance() {
        if (lazyMan == null) {
            synchronized (LazySingletonWithDCL.class) {
                if (lazyMan == null) {
                    lazyMan = new LazySingletonWithDCL();
                }
            }
        }
        return lazyMan;
    }

    public static void main(String[] args) throws Exception {
        LazySingletonWithDCL instance = LazySingletonWithDCL.getInstance();
        System.out.println(instance); //创建成功: LazySingletonWithDCL@61bbe9ba
        //利用反射创建对象
        //获取无参构造
        Constructor<LazySingletonWithDCL> declaredConstructor = LazySingletonWithDCL.class.getDeclaredConstructor(null);
        //开放权限
        declaredConstructor.setAccessible(true);
        LazySingletonWithDCL instance_withReflect = declaredConstructor.newInstance();

        System.out.println(instance_withReflect);  //LazySingletonWithDCL@610455d6
        System.out.println(instance == instance_withReflect);  //false
    }
}
main ->OK
com.kuangstudy.Singleton.LazySingletonWithDCL@61bbe9ba
Exception in thread "main" java.lang.reflect.InvocationTargetException
	at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
	at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62)
	at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
	at java.lang.reflect.Constructor.newInstance(Constructor.java:423)
	at com.kuangstudy.Singleton.LazySingletonWithDCL.main(LazySingletonWithDCL.java:60)
Caused by: java.lang.RuntimeException: 不要试图通过反射破解单例
	at com.kuangstudy.Singleton.LazySingletonWithDCL.<init>(LazySingletonWithDCL.java:20)
	... 5 more

只创建出了一个实例,并成功拦截了 通过反射创建对象的行为

  1. 反射再扳回一城

上述例子2中是用一个饿汉+反射创建,但如果两个对象都是用反射创建呢?

public static void main(String[] args) throws Exception {
    //获取类模板class
    Constructor<LazySingletonWithDCL> declaredConstructor = LazySingletonWithDCL.class.getDeclaredConstructor(null);
    //开放权限
    declaredConstructor.setAccessible(true);
    //通过空参创建实例
    LazySingletonWithDCL instance_withReflect1 = declaredConstructor.newInstance();
    LazySingletonWithDCL instance_withReflect2 = declaredConstructor.newInstance();
    System.out.println(instance_withReflect1);//LazySingletonWithDCL@61bbe9ba
    System.out.println(instance_withReflect2);//LazySingletonWithDCL@610455d6
    System.out.println(instance_withReflect1 == instance_withReflect2);//false
}

结果非常明显:再一次破坏了单例模式

  1. 懒汉单例再度防守:

    (加入标志位)

    在空参方法里运用标志位,因为实例化对象需要获取类模板:

    • 当获取了一次类模板之后,就把标志位flag置反。
    • 等一次通过反射获取类模板创建对象的时候便能抛异常
public class LazySingletonWithDCL {

    //7.加入标志位,防止多个反射破坏单例
    private static boolean flag = true;

    //1.私有化构造函数
    //6.增加对反射的判断
    private LazySingletonWithDCL() {
        synchronized (LazySingletonWithDCL.class){
            if (flag){
                flag = false;
            }else {
                //                //6.1如果此时已经有实例,阻止反射创建
                //                if (lazyMan != null){
                throw new RuntimeException("不要试图通过反射破解单例");
                //                }
            }

        }
        System.out.println(Thread.currentThread().getName() + " ->OK");
    }

    //2.创建对象(容器)
    //    private static LazySingletonWithDCL lazyMan ;
    //5.new 不是一个原子性操作:
    private volatile static LazySingletonWithDCL lazyMan;

    //3.对外提供静态实例化方法
    //4.为保证线程安全,需要上锁
    public static LazySingletonWithDCL getInstance() {
        if (lazyMan == null) {
            synchronized (LazySingletonWithDCL.class) {
                if (lazyMan == null) {
                    lazyMan = new LazySingletonWithDCL();
                }
            }
        }
        return lazyMan;
    }

    public static void main(String[] args) throws Exception {
        //获取类模板class
        Constructor<LazySingletonWithDCL> declaredConstructor = LazySingletonWithDCL.class.getDeclaredConstructor(null);
        //开放权限
        declaredConstructor.setAccessible(true);
        //通过空参创建实例
        LazySingletonWithDCL instance_withReflect1 = declaredConstructor.newInstance();
        System.out.println(instance_withReflect1);//LazySingletonWithDCL@61bbe9ba

        LazySingletonWithDCL instance_withReflect2 = declaredConstructor.newInstance();
        System.out.println(instance_withReflect2);
        //报错:java.lang.RuntimeException: 不要试图通过反射破解单例
        System.out.println(instance_withReflect1 == instance_withReflect2);
    }
}

由运行结果可见,保住了单例模式。

至此,用过反射调用空参实例化的方法被标志位掐断

  1. 反射再度进攻

    (你既然设置了标志位,那我就来破坏标志位)

public static void main(String[] args) throws Exception{
    //获取标志位
    Field flag = LazySingletonWithDCL.class.getDeclaredField("flag");
    flag.setAccessible(true);

    //获取类模板class
    Constructor<LazySingletonWithDCL> declaredConstructor = LazySingletonWithDCL.class.getDeclaredConstructor(null);
    //开放权限
    declaredConstructor.setAccessible(true);
    //通过空参创建实例
    LazySingletonWithDCL instance_withReflect1 = declaredConstructor.newInstance();
    System.out.println(instance_withReflect1);//LazySingletonWithDCL@61bbe9ba

    //在获取并使用完类模板后,重新设置flag值:
    flag.set(instance_withReflect1,true);
    LazySingletonWithDCL instance_withReflect2 = declaredConstructor.newInstance();
    System.out.println(instance_withReflect2);

}

此时:

main ->OK
com.kuangstudy.Singleton.LazySingletonWithDCL@61bbe9ba
main ->OK
com.kuangstudy.Singleton.LazySingletonWithDCL@511d50c0

可以看到单例模式再一次被破坏了

  1. 该如何真正确保单例?

解铃还须系铃人,那我们就从单例入手分析一波:

查看反射中newInstance()方法

在这里插入图片描述

否则会报异常:

 IllegalArgumentException("Cannot reflectively create enum objects")

枚举保证单例:

  1. 构造枚举尝试单例模式:

public enum EnumSingleton {

    INSTANCE;

    public EnumSingleton getInstance(){
        return INSTANCE;
    }
}

class Test{

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

简单代码创建一个实例,针不戳

  1. 尝试用反射破坏枚举的单例模式

public static void main(String[] args) throws Exception {
    //instance1正常获取
    EnumSingleton instance1 = EnumSingleton.INSTANCE;
    System.out.println(instance1); //INSTANCE

    //instance2通过反射获取:
    //1.获取其空参
    Constructor<EnumSingleton> constructor = EnumSingleton.class.getDeclaredConstructor(null);
    //2.打开权限:
    constructor.setAccessible(true);
    //3.实例化对象:
    EnumSingleton instance2 = constructor.newInstance();
    System.out.println(instance2);
}

结果:

  1. instance1 正常实例化

  2. 但是在通过反射的时候报了错:

    在这里插入图片描述

    找不到EnumSingleton 的空参构造?

    这就奇怪了。

    idea查看class文件也是只有空参构造:

    在这里插入图片描述

    javap -p -c .\EnumSingleton.class口令也是看到空参:

    在这里插入图片描述

  3. 探究枚举类的构造函数:

    上述方法行不通后得运用更加专业的工具进行反编译:

    使用jad工具

    使用命令:

     .\jad.exe -sjava .\EnumSingleton.class
    

    将class成功反编译为Java文件:

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

    此时我们发现枚举类内部其实是一个有参的构造函数。

    此时我们修改代码:

     //1.获取其有参构造:
            Constructor<EnumSingleton> constructor = EnumSingleton.class.getDeclaredConstructor(String.class,int.class);
    

    这时候我们就能如愿得到想要的报错了 (?怎么听起来怪怪的)

    INSTANCE
    Exception in thread "main" java.lang.IllegalArgumentException: Cannot reflectively create enum objects
    	at java.lang.reflect.Constructor.newInstance(Constructor.java:417)
    	at com.kuangstudy.Singleton.Test.main(EnumSingleton.java:40)
    

    小结:

    用枚举构建单例模式:

    特点属性
    JDK 版本JDK1.5 起
    是否 Lazy 初始化
    是否多线程安全
    实现难度
  • 这种实现方式还没有被广泛采用,但这是实现单例模式的最佳方法。它更简洁,自动支持序列化机制,绝对防止多次实例化。
  • 这种方式是 Effective Java 作者 Josh Bloch 提倡的方式,它不仅能避免多线程同步问题,而且还自动支持序列化机制,防止反序列化重新创建新的对象,绝对防止多次实例化。不过,由于 JDK1.5 之后才加入 enum 特性,用这种方式写不免让人感觉生疏,在实际工作中,也很少用。
    不能通过 reflection attack 来调用私有构造方法。

单例模式在JDK中的应用:

在Runtime类中使用了单例模式(不过是俄汉式)上代码:

public class Runtime {
    //2.类的内部创建对象
    private static Runtime currentRuntime = new Runtime();

    //3.开发对外的实例化方法
    public static Runtime getRuntime() {
        return currentRuntime;
    }

    //1.私有化构造方法
    private Runtime() {}

    ....
}
  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值