• Java设计模式之单例模式详细讲解


    设计模式与单例模式

    1、什么是单例模式

    ​ 单例模式是指保证某个类在整个软件系统中只有一个对象实例,并且该类仅提供一个返回其对象实例的方法(通常为静态方法)

    2、单例模式的种类

    ​ 经典的单例模式实现方式一般有五种

    2.1 饿汉式
    // ①饿汉式:使用静态常量
    static class Singleton {
        // 1.构造器私有化,其他类不能new
        private Singleton() {}
        // 2.类的内部创建对象
        private final static Singleton instance = new Singleton();
        // 3.向外部暴露一个静态的公共方法
        public static Singleton getInstance() {
            return instance;
        }
    }
    // ②饿汉式:使用静态代码块
    static class Singleton {
        // 1.构造器私有化,其他类不能new
        private Singleton() {}
        private static final Singleton instance;
        // 2.静态代码块实例化
        static {
            instance = new Singleton();
        }
        // 3.向外部暴露一个静态的公共方法
        public static Singleton getInstance() {
            return instance;
        }
    }
    
    • 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

    ​ 饿汉式顾名思义就是迫不及待地加载该类的对象实例,对象实例的加载最早是在类的加载过程中的初始化阶段(即静态引用变量的加载,对应字节码文件中方法的执行),加载过程由JVM保证线程安全。饿汉式会浪费内存,但是随着计算机的发展,内存已经不是问题了,所以使用饿汉式也未尝不可。

    JDK源码举例:
    在这里插入图片描述
    ​ 该类位于java.lang包下,首先将构造方法私有化,声明了一个私有的静态变量并且对该变量进行对象实例的创建,再创建一个公有的静态方法返回这个对象实例,这是比较常用的一种实现单例模式的方式。

    2.2 懒汉式
    // ①懒汉式:线程不安全
    static class Singleton {
        // 1.构造器私有化,其他类不能new
        private Singleton() {}
        private static Singleton instance;
        // 2.向外部暴露一个静态的公共方法
        public static Singleton getInstance() {
            // 3.instance == null时进行实例化
            if ( instance == null ) {
                // new Singleton()不是一个原子操作,JVM中会进行大致[创建对象-分配内存-对象初始化]等过程,在这之前instance都为null
                // 多线程情况下,多个线程同时执行到该位置,线程获取到时间片后会继续执行,就可能创建多个实例
                instance = new Singleton();
            }
            return instance;
        }
    }
    // ②懒汉式:线程安全(方法上添加 synchronized 关键字)
    static class Singleton {
        // 1.构造器私有化,其他类不能new
        private Singleton() {}
        private static Singleton instance;
        // 2.向外部暴露一个静态的公共方法, synchronized 保证线程安全
        public static synchronized Singleton getInstance() {
            // 3.instance == null时进行实例化
            if ( instance == null ) {
                instance = new Singleton();
            }
            return instance;
        }
    }
    
    • 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

    懒汉式就是在创建对象实例前先判断是否已经创建,但是由于对象实例的创建并不是一个原子过程,所以会出现线程安全问题,可以在方法上添加synchronized解决,当然会牺牲一定的性能。基于以上原因,不推荐使用懒汉式的方式实现单例模式。

    ​ 如何证明对象实例的创建不是一个原子操作?字节码指令可以从侧面证明

    // Java源码
    public class Test {
        public Test getTest() {
            return new Test();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    在这里插入图片描述
    红框1的位置有三条字节码指令,这还只是字节码的层面,再往低层还会有更多的步骤,所以很明显对象实例的创建不是一个原子操作

    2.3 双重检查锁
    static class Singleton {
        // 1.构造器私有化,其他类不能new
        private Singleton() {}
        // 2.volatile保证多线程下的可见性
        private static volatile Singleton instance;
        // 3.向外部暴露一个静态的公共方法
        public static Singleton getInstance() {
            // 3.非空判断
            if ( instance == null ) {
                // 4.同步代码块
                synchronized (Singleton.class) {
                    // 5.再次非空判断(保证多线程下的单例)
                    if ( instance == null ) {
                        instance = new Singleton();
                    }
                }
            }
            return instance;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    双重检查锁是最复杂的一种单例模式实现方式,我把它拆分成三个问题:

    ​ ① 为什么synchronized不加到方法上?

    ​ 如果添加到方法上,两次非空判断就没有必要了,一次就够了,就转化成了懒汉式(线程安全),这种方式效率不高,因为每次调用都需要获取锁和释放锁。

    ​ ② 为什么要做两次非空判断?

    ​ 之前也提到过:对象实例的创建不是一个原子操作。线程安全问题也是出在这一过程中的,解决方案就是添加synchronized关键字,但是添加到方法上效率又太低了;

    ​ 既然问题是出现在对象实例创建的过程中,那么只对这一段代码进行同步操作(加锁对象就是当前的Class对象,因为对象实例只有一个);

    ​ 第一层的非空判断是为了如果对象实例已经创建完成了,就不需要再次进入同步代码块了,直接返回创建好的对象实例即可。

    ​ ③ 为什么要加volatile?
    在这里插入图片描述
    根据对象实例创建的字节码指令可以看出对象实例的创建大致分为三步:

    ​ ① 在堆内存中分配对象内存

    ​ ② 调用方法,执行对象实例的初始化

    ​ ③ 将对象引用赋给静态变量

    大家应该对JMM模型和happens-before有所了解,简单来说JMM模型是对编译器和处理器的约束,happens-before是对开发者的约束。

    编译器和处理器在实际运行时,为了执行效率可能会对指令进行重排序的操作,虽然单线程中不会影响执行结果,但是如果是多线程就会出现问题。

    像对象实例创建过程的三条指令中②③就有可能会被优化为③②,但是①一定会先执行,因为②③依赖于①,此时执行顺序为①③②,其他线程就会获取到一个未初始化的对象,导致执行出错。

    而volatile关键字的语义包含两个:

    ​ ① 保证可见性

    ​ ② 禁止指令重排序(所以添加volatile后,执行顺序就是①②③了)

    ​ JDK源码举例:
    在这里插入图片描述
    该类是位于java.lang包下的System类,经典的双重检查锁实现方式。

    2.4 静态内部类
    static class Singleton {
        // 1.构造器私有化,其他类不能new
        private Singleton() {}
        // 2.静态内部类,Singleton类加载的时候不会加载内部类,只有用到内部类时才回去加载内部类(保证懒加载)
        private static class SingletonInstance {
            private static final Singleton instance = new Singleton();
        }
        // 3.向外部暴露一个静态的公共方法,此时会装载SingletonInstance,类装载时是线程安全的(保证线程安全)
        public static Singleton getInstance() {
            return SingletonInstance.instance;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    ​ 这是一种很巧妙的方式,相对于饿汉式来说,不需要在类的初始化阶段就创建对象实例,只有在需要(即调用getInstance()方法)的时候才会进行对象实例创建,线程安全也由JVM保证。

    ​ JDK源码举例:
    在这里插入图片描述

    ​ 上图是java.lang.Short源码中的内部类,将常用的整数保存到缓存池当中;下图是访问缓存池中的整数。类似的还有java.lang.Integer,java.lang.Long等包装类。

    在这里插入图片描述

    2.5 枚举
    // enum实际上是extends抽象类java.lang.Enum
    enum Singleton {
        instance
    }
    
    • 1
    • 2
    • 3
    • 4

    字节码反编译看下:
    在这里插入图片描述

    enum关键字修饰的类实际上继承了java.lang.Enum 在这里插入图片描述
    枚举类中声明的实例实际上是public static final修饰的常量
    在这里插入图片描述
    上图为枚举类中方法的字节码指令,也就是类的初始化阶段需要执行的逻辑(即将静态变量,静态代码块整合到一块)。

    ​ 红框1:创建Singleton枚举类对象实例,实际上调用了java.lang.Enum类的构造器(即方法),构造器参数是(“INSTANCE”, 0),可以通过ldc #7和iconst_0看出来;对象实例创建完成后,将实例引用赋给INSTANCE常量。

    ​ 红框2:将上一步创建的对象实例引用保存到枚举类内部数组$VALUES中,外部可以通过values()方法返回所有的枚举对象引用;数组的创建是在iconst_1和anewarray,意思是创建一个长度为1的引用类型数组。

  • 相关阅读:
    Latex语法学习08:打通latex、mathml和word公式转换
    2022年最新辽宁建筑八大员(标准员)考试试题及答案
    NT68661-屏参升级-RK3128-开机自己升级屏参
    详解大模型是如何理解并使用 tools ?
    DEM可视化如何更具有高级感
    Hash Table Mock
    LeetCode·每日一题·1374.生成每种字符都是奇数个的字符串·模拟
    c++-红黑树
    uboot启动流程-board_init_r函数执行过程
    C++17开始取消std::codecvt_utf8支持
  • 原文地址:https://blog.csdn.net/qq_42342282/article/details/127691288