• 【Java设计模式】二、单例模式


    设计模式即总结出来的一些最佳实现。GoF(四人组) 书中提到23种设计模式,可分为三大类:

    • 创建型模式:隐藏了创建对象的过程,通过逻辑方法进行创建对象,使用者不用关注对象的创建细节(对那种属性很多,创建麻烦的对象尤其好用)
    • 结构型模式:主要关注类和对象的组合关系。将类或对象按某种布局组成更大的结构
    • 行为型模式:主要关注对象之间的通信与配合

    在这里插入图片描述

    0、单例模式

    • 单例模式即在程序中想要保持一个实例对象,让某个类只有一个实例
    • 单例类必须自己创建自己唯一的实例,并对外提供
    • 优点:减少了内存开销

    单例模式的实现,有以下几种思路:

    1、饿汉式

    • 类加载就会导致该单实例对象被创建
    • 通过静态代码块或者静态变量直接初始化

    方式一:静态成员变量的方式

    public class HungrySingleton {
    
        private static final HungrySingleton hungrySingleton = new HungrySingleton();
    	
    	//私有的构造方法,只能本类调用,不给外界用
        private HungrySingleton(){
    
        }
    	//提供一个获取实例的方法给外界
        public static HungrySingleton getInstance(){
            return hungrySingleton;
        }
    }
    
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    方式二:静态代码块

    public class HungrySingleton {
    
       
        private static HungrySingleton hungrySingleton = null;
    
        //  静态代码块中进行赋值
        static {
            hungrySingleton = new HungrySingleton();
        }
    	//私有的构造方法,只能本类调用,不给外界用
        private HungrySingleton(){
    
        }
    	//提供一个获取实例的方法给外界
        public static HungrySingleton getInstance(){
            return hungrySingleton;
        }
    }
    
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    以上两种方式,对象会随着类的加载而创建,如果这个对象后来一直没被用,就有点白占内存了。

    2、懒汉式

    类加载不会导致该单实例对象被创建,而是首次使用该对象时才会创建


    懒汉式方式一:线程不安全
    public class LazySingleton  {
        private static LazySingleton lazySingleton = null;
    
        /**
         * 私有的构造方法,保证出了本类就不能再被调用,以防直接去创建对象
         */
        private LazySingleton() {
    
        }
    
        /**
         * 单例对象的获取
         */
        public static LazySingleton getInstance() {
            if (lazySingleton == null) {
                lazySingleton = new LazySingleton();
            }
            return lazySingleton;
        }
    
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    测试类启两个线程获取对象:

    public class Test {
        public static void main(String[] args) {
            new Thread(() -> {
                LazySingleton instance = LazySingleton.getInstance();
                System.out.println(Thread.currentThread().getName() + "-->" + instance);
            }, "t1").start();
            new Thread(() -> {
                LazySingleton instance = LazySingleton.getInstance();
                System.out.println(Thread.currentThread().getName()+ "-->" + instance);
            }, "t2").start();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    发现可能出现获取到两个不同对象的情况,这是因为线程安全问题:

    在这里插入图片描述

    两个线程A、B,同时执行IF 这一行,被挂起,再被唤醒时继续往下执行,就会创建出两个不同的实例对象。那最先想到的应该是synchronized关键字解决,但这样性能低下,因为不管对象是否为null,每次都要等着获取锁。

    //性能低下,一刀切,不可行
    public static synchronized LazySingleton getInstance() {
         if (lazySingleton == null) {
             lazySingleton = new LazySingleton();
         }
         return lazySingleton;
     }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    3、双重检查

    通过两个IF判断,加上同步锁进行实现。(懒汉式方式二:双重检查)

    public class DoubleCheckSingleton {
    
        private static DoubleCheckSingleton doubleCheckSingleton;
    
        /**
         * 私有的构造方法,保证出了本类就不能再被调用,以防直接去创建对象
         */
        private DoubleCheckSingleton(){
    
        }
        public static DoubleCheckSingleton getInstance(){
            if(doubleCheckSingleton == null){
                synchronized (DoubleCheckSingleton.class){
                    if(doubleCheckSingleton == null){
                        doubleCheckSingleton = new DoubleCheckSingleton();
                    }
                }
            }
            return doubleCheckSingleton;
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    如此,再有A、B两个线程同时执行到第一个IF,只能有一个成功创建对象,另一个获取到锁后,第二重判断会告诉它已经有对象实例了。而亮点则在于,对象初始化完成后,后面来获取对象的线程不用等着拿锁,第一个IF就能告诉它已有对象,不用再等锁了。


    以上双端检锁虽然线程安全,但问题是,JVM指令重排序后,可能出现空指针异常,可再加volatile关键字(volatile的可见性和有序性,这里用它的有序性)

    //...
    private static volatile DoubleCheckSingleton doubleCheckSingleton;
    //...
    
    • 1
    • 2
    • 3

    4、静态内部类

    在单例类中,通过私有的静态内部类,创建单例对象(加private修饰词的,出了本类无法调用和访问)。这也是懒汉式的第三种方式。

    public class StaticInnerClassSingleton {
    
        /**
         * 私有的静态内部类实例化对象
         * 给内部类的属性赋值一个对象
         */
        private static class InnerClass{
            private static StaticInnerClassSingleton staticInnerClassSingleton = new StaticInnerClassSingleton();
        }
    
        /**
         * 私有的构造方法,保证出了本类就不能再被调用,以防直接去创建对象
         */
        private StaticInnerClassSingleton(){
    
        }
    	/**
         * 对外提供获取实例的方法
         */
        public static StaticInnerClassSingleton getInstance(){
            return InnerClass.staticInnerClassSingleton;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    外部类StaticInnerClassSingleton被加载时,其对象不一定被初始化,因为内部类没有被主动使用到。直到调用getInstance方法时,静态内部类InnerClass被加载,完成实例化。

    静态内部类在被加载时,不会立即实例化,而是在第一次使用时才会被加载并初始化。

    JVM在加载外部类的过程中,不会加载静态内部类,只有内部类的属性或方法被调用时才会被加载,并初始化其静态属性。 这种延迟加载的特性,使得我们可以通过静态内部类来实现在需要时创建单例对象。这种方式没有线程安全问题,也没有性能影响和空间的浪费。

    5、枚举

    • 单例模式的最佳实现方式
    • 枚举类型是线程安全的,并且只会装载一次
    • 可有效防止对单例模式的破坏
    • 属于饿汉式
    public enum EnumSingleton {
        INSTANCE;
    
        public static EnumSingleton getInstance(){
            return INSTANCE;
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    6、单例模式的破坏:序列化和反序列化

    通过流将单例对象,序列化到文件中,然后再反序列化读取出来。发现通过反序列化方式创建出来的对象内存地址,和原对象不一样。或者对象序列化到文件后,两次反序列化得到的对象也不一样,单例模式被破坏。

    public class TestSerializer {
    
        public static void main(String[] args) throws Exception {
            //懒汉式
            LazySingleton instance = LazySingleton.getInstance();
            //把对象序列化到文件中
            ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("singleton"));
            oos.writeObject(instance);
            //从文件中反序列化对象
            ObjectInputStream ois = new ObjectInputStream(new FileInputStream("singleton"));
            LazySingleton objInstance = (LazySingleton) ois.readObject();
            System.out.println(instance);
            System.out.println(objInstance);
            System.out.println(instance == objInstance);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    在这里插入图片描述

    可以发现单例模式的五种实现方式中,只有枚举不会被破坏单例模式。如果非要用其他几种模式,可以加readResolve方法来重写反序列化逻辑。因为反序列化创建对象时,是通过反射创建的,反射会调用readResolve方法,并将其返回值做为反序列化的结果。 没有重写readResolve方法时,会通过反射创建一个新的对象,从而破坏了单例模式。这一点在对象流ObjectInputStream的源码可看出:

    在这里插入图片描述

    public class LazySingleton implements Serializable {
        private static LazySingleton lazySingleton = null;
    
        /**
         * 私有的构造方法,保证出了本类就不能再被调用,以防直接去创建对象
         */
        private LazySingleton() {
        }
    
        /**
         * 单例对象的获取
         */
        public static LazySingleton getInstance() {
            if (lazySingleton == null) {
                lazySingleton = new LazySingleton();
            }
            return lazySingleton;
        }
    
        private Object readResolve(){
            return lazySingleton;
        }
    
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25

    在这里插入图片描述

    也可考虑使用@JsonCreator注解。

    7、单例模式的破坏:反射

    • 通过字节码对象,创建构造器对象
    • 通过构造器对象,初始化单例对象
    • 由于单例对象的构造方法是private私有的,调用构造器中的方法,赋予权限,创建单例对象

    注意私有修饰词时,反射会IllegalAccessException

    在这里插入图片描述

    处理下private问题,用懒汉模式验证:

    public class TestReflect {
        public static void main(String[] args) throws Exception{
            //创建字节码对象
            Class<LazySingleton> clazz = LazySingleton.class;
            //构造器对象
            Constructor<LazySingleton> constructor = clazz.getDeclaredConstructor();
            //赋予权限
            constructor.setAccessible(true);
            //解决了私有化问题,获取实例对象
            LazySingleton o1 = constructor.newInstance();
            //再次反射
            LazySingleton o2 = constructor.newInstance();
            System.out.println(o1);
            System.out.println(o2);
            System.out.println(o1 == o2);
    
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    在这里插入图片描述

    用枚举的方式验证:

    public class TestEnumReflect {
        public static void main(String[] args) throws Exception {
            Class<EnumSingleton> clazz = EnumSingleton.class;
            //枚举下的单例模式,创建构造方法时,需要给两个参数,薮泽NoSuchMethodException
            //这两个参数是源码中的体现,一个是String,一个是int
            Constructor<EnumSingleton> constructor = clazz.getDeclaredConstructor(String.class, int.class);
            constructor.setAccessible(true);
            EnumSingleton instanceReflect = constructor.newInstance("test",1234);
            EnumSingleton instanceSingleton = EnumSingleton.getInstance();
            System.out.println(instanceReflect);
            System.out.println(instanceSingleton);
            System.out.println(instanceReflect == instanceSingleton);
    
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    运行报错:Cannot reflectively create enum objects,即反射创建枚举的单例对象,是不允许的:

    在这里插入图片描述

    在其他单例模式的实现方式里,也可以实现不允许通过反射创建对象,反射靠拿构造方法对象,调整下构造方法(比如双重检锁):

    public class DoubleCheckSingleton {
    
        private static DoubleCheckSingleton doubleCheckSingleton;
    
        /**
         * 私有的构造方法,保证出了本类就不能再被调用,以防直接去创建对象
         */
        private DoubleCheckSingleton(){
    		if (doubleCheckSingleton != null) {
    			throw new RuntimeException("不允许创建多个对象");
    		}
        }
        public static DoubleCheckSingleton getInstance(){
            if(doubleCheckSingleton == null){
                synchronized (DoubleCheckSingleton.class){
                    if(doubleCheckSingleton == null){
                        doubleCheckSingleton = new DoubleCheckSingleton();
                    }
                }
            }
            return doubleCheckSingleton;
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    或者:

    在这里插入图片描述

    8、单例模式的实际应用

    JDK的Runtime类就是饿汉式的单例模式:

    在这里插入图片描述

    PS:Runtime类的简单使用 --> 执行DOS命令并获取结果

    public class RuntimeDemo {
    	public static void main(String[] args) throws IOException {
    		//获取Runtime类对象
    		Runtime runtime = Runtime.getRuntime();
    		//返回 Java 虚拟机中的内存总量。
    		System.out.println(runtime.totalMemory());
    		//返回 Java 虚拟机试图使用的最大内存量。
    		System.out.println(runtime.maxMemory());
    		//创建一个新的进程执行指定的字符串命令,返回进程对象
    		Process process = runtime.exec("ipconfig");
    		//获取命令执行后的结果,通过输入流获取
    		InputStream inputStream = process.getInputStream();
    		byte[] arr = new byte[1024 * 1024* 100];
    		//将流输入到数组,返回读到的字节个数
    		int b = inputStream.read(arr); 
    		//字节数组转字符串,指定下字符集为GBK
    		System.out.println(new String(arr,0 ,b ,"gbk"));
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
  • 相关阅读:
    SpringBoot官方支持任务调度框架,轻量级用起来也挺香!
    Kotlin高仿微信-第32篇-支付-我的零钱
    java基于ssm的健身房会员管理系统
    【笔记篇】10仓管系统出库管理——之《实战供应链》
    android 开机动画制作
    jquery 分页兼容i7,i8浏览器
    AK F.*ing leetcode 流浪计划之delaunay三角化
    Delta tuning(只抓核心)
    语音人工智能的简单介绍
    产品经理学习笔记
  • 原文地址:https://blog.csdn.net/llg___/article/details/136302439