• Java - 你真的明白单例模式怎么写了吗?


    前言

    单例模式我们在日常的开发中用到的还是比较多的,比如线程池的创建。我们也知道单例模式有很多种写法,饿汉式、懒汉式、枚举等等巴拉巴拉的。但是我们可能对每种写法的区别又不太了解,那么本篇文章就准备着重讲解下各个单例的写法。

    一. 单例模式

    1.1 饿汉式

    饿汉式:类在加载的时候就会创建实例。

    主要的特点如下:

    1. 构造函数私有化(防止外部调用构造器创建实例)。
    2. 提供一个静态的单例对象成员,并直接调用构造初始化。
    3. 会随着类的加载就创建单例。 (只要用到这个类,就会创建单例对象)
    public class Singleton {
        // 构造函数私有化,只允许本类内部构建实例
        private Singleton() {
        	System.out.println("构造调用");
        }
        // 通过static修饰的成员对象,在加载类的时候就会执行。
        private static Singleton instance = new Singleton();
        // 提供静态的公共方法,将单例对象返回给外部
        public static Singleton getInstance() {
            return instance;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    测试如下:可以见到构造函数只调用了一次。两次获得的实例也是同一个对象。
    在这里插入图片描述
    饿汉式的优点:线性安全的。

    饿汉式的缺点:倘若某个单例构造函数的耗时比较久,而这个单例使用频率不高或者还没到需要创建实例的时候,那么根据饿汉式的写法,无论是否用到实例对象,都会创建该实例,浪费资源和时间。

    举个例子:在原有代码上,增加一个静态方法hello(),外部去执行:

    public static void hello() {
        System.out.println("Hello");
    }
    
    • 1
    • 2
    • 3

    当执行Singleton.hello();的时候,打印结果如下;
    在这里插入图片描述

    有个细节大家有没有注意到,我们发现有的时候饿汉式的getInstance()加了synchronized修饰。而这里没有,我个人觉得可以这么看:把synchronized关键字和 多线程 这三个字关联起来。

    • 高并发:就加synchronized修饰。
    • 单线程:不加也可,加也可(没必要加)。

    我们知道,静态方法是属于这个类的,而非这个实例,那么我们调用这个类方法的时候,竟然创建出对于的实例对象,对于上述场景,这个实例即使创建出来,我们也是用不上的,这就是饿汉式的一个诟病。因此就有了懒汉式的单例写法。

    1.2 懒汉式

    懒汉式和饿汉式的一个最大不同就是:实例的懒加载,即并非在类加载的时候创建单例对象,而是在你调用创建对象实例的方法时才会创建。

    主要的特点如下:

    1. 构造函数私有化。(同样的目的,防止外部调用构造函数创建实例)
    2. 提供一个静态的单例对象成员,但是并没有初始化,一开始为null
    3. 只有调用对象实例方法的时候才会创建单例对象。

    我们来看下代码:

    public class LazySingleton {
        // 初始化为null
        private static volatile LazySingleton instance = null;
    
        private LazySingleton() {
            System.out.println("构造调用");
        }
    
        // 在调用对象实例的时候,返回对应的实例
        public static LazySingleton getInstance() {
            // 双重检锁,第一次判空
            if (instance == null) {
                synchronized (LazySingleton.class) {
                    // 第二次判空
                    if (instance == null) {
                        // 创建实例并返回
                        instance = new LazySingleton();
                    }
                }
            }
            return instance;
        }
    
        public static void hello() {
            System.out.println("Hello");
        }
    }
    
    • 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
    • 26
    • 27

    我们先来执行一下LazySingleton.hello();代码看看结果:可见并没有构造实例化出一个单例对象。
    在这里插入图片描述
    然后我们再来分析一下代码,为什么要这么写,有什么隐患:

    1. 首先有一点很明确,instance实例的构造函数只有在getInstance()执行的时候才可能发生,因此instance在最开始的时候并不会被实例化,符合懒汉式的语义。
    2. 其次,在多线程并行的情况下,关于instance = new LazySingleton();肯定是要加锁的,不然可能会重复执行。因此用synchronized (LazySingleton.class)锁住了这个类。
    3. 第一次判空,目的是为了减少synchronized 加锁带来的开销,因为倘若这个实例已经被初始化,那么就没必要加锁了走后续的判断,可以直接返回结果。
    4. 第二次判空:目的是为了校验instance实例是否被创建,没创建则创建,否则直接返回。
    5. private static volatile LazySingleton instance = null;为什么要加volatile呢?因为执行instance = new Singleton()这段代码的时候,实际上会有三大步骤,volatile是用于禁止重排序的。
    memory = allocate(); // 1.分配对象的内存空间
    ctorInstance(memory);// 2.初始化对象
    instance = memory;// 3.设置instance指向刚分配的内存地址
    
    • 1
    • 2
    • 3

    其实我写到这里的时候,才想起自己以前写过的文章Java中的双重检索与延迟初始化,里面对于上述第五点讲的比较详细,也就是懒汉式的双重检索的细节讲解。现在回首,确实多看书和学习还是有帮助的,很多知识都是能互相串起来的。

    1.3 静态内部类

    我们知道,单例模式,单例单例,就是希望每次获得的对象实例都是同一个**,那么我们就忌惮单例被重复初始化。** 但是我们又知道,Java类只会被加载一次。因此又有了静态内部类的单例模式写法:

    public class InterSingleton {
        // 私有构造函数
        private InterSingleton() {
            System.out.println("构造调用");
        }
    
        // 静态内部类
        private static class SingletonHolder {
            private static final InterSingleton instance = new InterSingleton();
        }
    
        public static InterSingleton getInstance() {
            return SingletonHolder.instance;
        }
    
        public static void hello() {
            System.out.println("Hello");
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    原理大概如下:

    1. 静态内部类单例模式中实例由内部类创建。
    2. 由于 JVM 在加载外部类的过程中, 是不会加载静态内部类的只有内部类的属性/方法被调用时才会被加载并初始化其静态属性。
    3. 静态属性由于被 static 修饰,保证只被实例化一次,并且严格保证实例化顺序。

    注意一点:重要的事情说三遍

    • 静态内部类并不是饿汉式,而是懒汉式加载!
    • 静态内部类并不是饿汉式,而是懒汉式加载!
    • 静态内部类并不是饿汉式,而是懒汉式加载!

    验证1:调用类方法:
    在这里插入图片描述
    验证2:调用getInstance获取实例时:
    在这里插入图片描述

    1.4 存在的问题

    首先我们从上述三种单例模式中,可以发现一个共通点:构造函数全部都是私有化的。

    为啥要私有化呢?

    1. 我们知道,Java对象实例的创建,就是Singletonxxx = new Singleton(),大部分都是调用其构造函数,若没有显式在类中定义,默认有个无参构造。
    2. 这行代码是创建对象实例的。那么问题来了,既然我们要求对某一个类的访问都是单例模式,那么我们自然而然要防止 new 出一个新的对象实例。
    3. 因此必须强制程序员调用暴露出来的getInstance()函数去获得指定的实例。而在编写new Singleton()的时候,由于构造函数是私有化的,就会报错,如图:
      在这里插入图片描述
      当然,上述的构造函数私有化是必须要这么写的,也算是个规范,它并不是问题,希望大家区分一下。那么真正的问题在哪?Java有一个反射机制,私有构造函数在它面前啥也不是!

    Java-通过反射来打印类,好巧不巧,我也写过反射相关的文章)

    废话不多说,上代码:

    @org.junit.Test
    public void tt() throws Exception {
        Class<?> aClass = Class.forName(Singleton.class.getName());
        Constructor<?> constructor = aClass.getDeclaredConstructors()[0];
        constructor.setAccessible(true);
        Singleton obj = (Singleton) constructor.newInstance();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    运行结果如下(反射的是用饿汉式写的单例类):
    在这里插入图片描述
    我们可以发现,它调用了两次构造!两次!两次!

    • 第一次:反射加载这个类,饿汉式的写法就会执行一次构造。
    • 第二次:利用反射机制手动调用了构造函数。

    总结下就是:饿汉式、懒汉式、静态内部类这三种单例模式,在反射机制的面前,依旧无法保证真正的单例。当然啊,一般人不会闲的没事通过反射获取类实例,绕过本身的单例机制。

    那么怎么办呢?有一种更好的方式:枚举类单例。

    二. 枚举类单例以及实操

    首先我们来根据上述的反射机制,来看下相关源码:

    2.1 反射源码(构造函数部分)

    我们来看下constructor.newInstance();这个源码:

    @CallerSensitive
    public T newInstance(Object ... initargs)
        throws InstantiationException, IllegalAccessException,
               IllegalArgumentException, InvocationTargetException
    {
        if (!override) {
            if (!Reflection.quickCheckMemberAccess(clazz, modifiers)) {
                Class<?> caller = Reflection.getCallerClass();
                checkAccess(caller, clazz, null, modifiers);
            }
        }
        // 重点看这里,看这里
        if ((clazz.getModifiers() & Modifier.ENUM) != 0)
            throw new IllegalArgumentException("Cannot reflectively create enum objects");
        ConstructorAccessor ca = constructorAccessor;   // read volatile
        if (ca == null) {
            ca = acquireConstructorAccessor();
        }
        @SuppressWarnings("unchecked")
        T inst = (T) ca.newInstance(initargs);
        return inst;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    if ((clazz.getModifiers() & Modifier.ENUM) != 0)判断了当前类是否是枚举类型,是就会抛出异常出来,就不能够通过反射来执行构造函数。也就是说,通过枚举类写的单例,不会有上述的情况。

    2.2 枚举类单例模板

    public class EnumSingleton {
    
        private enum singleton {
            // 用来暴露给外层去执行的对象
            INSTANCE;
            // 我们的目的是获取EnumSingleton这个单例实例,
            private final EnumSingleton enumSingleton;
    
            // 枚举中,倘若定义了成员对象,必须要有初始化的动作,一般都放在构造里面
            singleton() {
                System.out.println("调用构造");
                enumSingleton = new EnumSingleton();
            }
    		// 实例方法,相当于INSTANCE是个实例,getEnumSingleton属于它的方法
            private EnumSingleton getEnumSingleton() {
                return enumSingleton;
            }
        }
    
        public static EnumSingleton getInstance() {
            return singleton.INSTANCE.getEnumSingleton();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    测试:

    @org.junit.Test
    public void tt() throws Exception {
        EnumSingleton instance = EnumSingleton.getInstance();
        EnumSingleton instance2 = EnumSingleton.getInstance();
        System.out.println(instance==instance2);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    运行结果如下:
    在这里插入图片描述

    2.3 单例线程池(枚举类)

    public class ThreadPoolUtils {
        public static final int MAX_POOL_SIZE = 16;
        public static final int CORE_POOL_SIZE = 8;
        public static final int KEEP_ALIVE_TIME = 3;
    
        // 枚举单例
        private enum ThreadEnum {
            INSTANCE;
            private final ThreadPoolExecutor executor;
            private final ThreadPoolUtil threadPoolUtil;
    
            ThreadEnum() {
                // 自定义线程名称
                ThreadFactory threadFactory = new ThreadFactoryBuilder().setNameFormat("base_data_translate_task_%d").build();
    			// 初始化线程池
                executor = new ThreadPoolExecutor(ThreadPoolUtil.CORE_POOL_SIZE,
                        ThreadPoolUtil.MAX_POOL_SIZE,
                        ThreadPoolUtil.KEEP_ALIVE_TIME,
                        TimeUnit.SECONDS,
                        new ArrayBlockingQueue<Runnable>(100),
                        threadFactory,
                        new ThreadPoolExecutor.AbortPolicy());
    			// 创建单例对象
                threadPoolUtil = new ThreadPoolUtil();
            }
    		// 获取线程池单例
            private ThreadPoolExecutor getThreadPool() {
                return executor;
            }
    
            private ThreadPoolUtil getThreadPoolUtil() {
                return threadPoolUtil;
            }
        }
    
        public static ThreadPoolUtil getInstance() {
            return ThreadEnum.INSTANCE.getThreadPoolUtil();
        }
    
        public ThreadPoolExecutor getThreadPool() {
            return ThreadEnum.INSTANCE.getThreadPool();
        }
    }
    
    • 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
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43

    调用:

    ThreadPoolExecutor executor = ThreadPoolUtil.getInstance().getThreadPool();
    
    • 1
  • 相关阅读:
    麦芽糖-链霉亲和素maltose-Streptavidins链霉亲和素-PEG-麦芽糖
    基于关联规则的网络信息安全风险度量分析模型
    【探索Linux】—— 强大的命令行工具 P.14(进程间通信 | 匿名管道 | |进程池 | pipe() 函数 | mkfifo() 函数)
    2023.10.20
    SpringCloud:使用Nacos作为配置中心
    一文详细讲解 io_uring
    Selenium 是什么?简单了解Selenium
    训练神经网络的详细步骤,神经网络训练过程图解
    Linux 软链接 与 硬链接 的区别
    2022CTF培训(五)字符串混淆进阶&代码自解密
  • 原文地址:https://blog.csdn.net/Zong_0915/article/details/126032727