• 单例模式的进阶之旅


    单例模式

    单例模式(Singleton)是最简单又最实用的设计模式之一,《设计模式——可复用面向对象软件的基础》一书中这样描述单例模式:

    1. 意图

    保证一个类仅有一个实例,并提供一个访问它的全局访问点。

    1. 动机

    ...让类自身负责保存它的唯一实例。这个类可以保证没有其他实例可以被创建(通过截取创建新对象的请求),并且它可以提供一个访问该实例的方法。这就是Singleton模式。

    简单的单例模式

    在Java中,一个最简单的单例模式是这样的:

    public class Singleton1 {
    
        // 创建这个类的唯一实例
        private static Singleton1 instance = new Singleton1();
    
        // 构造方法私有化,禁止外部创建实例
        private  Singleton1() {}
    
        // 提供一个访问点用于获取单例
        public static Singleton1 getInstance() {
            return instance;
        }
    }
    

    懒加载

    业务中可能会有这样的需求:这个单例不一定会被调用,如果一开始就将其实例化的话,会有浪费空间的可能。因此,我们需要在调用到getInstance()方法时再实例化单例。

    public class Singleton2 {
    
        // 只声明不初始化
        private static Singleton2 instance;
    
        private  Singleton2() {}
        
        public static Singleton2 getInstance() {
            // 判断是否已被初始化
            if (instance == null){
                instance = new Singleton2();
            }
            return instance;
        }
    }
    

    并发安全

    对于上一种单例模式的实现,在并发情景下,如果在一个线程判断了instance==null,而尚未实例化instance之际,另一个线程也走到了instance==null这一步,那么仍然会判断为true,导致的后果就是两个线程分别实例化了一个instance,这违背了我们使用单例模式的初衷。想要避免这种情况,也很简单,就是给getInstance()方法加上synchronized关键字,保证这是一个同步方法。

    public class Singleton3 {
    
        private static Singleton3 instance;
    
        private Singleton3() {}
    
        public synchronized static Singleton3 getInstance() {
            if (instance == null){
                instance = new Singleton3();
            }
            return instance;
        }
    }
    

    保证并发安全后的效率问题

    上一种实现中,调用getInstance()方法时会导致整个方法被锁住,如果这个方法中还有一些比较耗时的业务代码的话,程序运行的效率都会受到比较大的影响,因此,我们需要缩小synchronized作用的范围。

    public class Singleton4 {
    
        private static Singleton4 instance;
    
        private Singleton4() {}
    
        public static Singleton4 getInstance() {
    
            // 业务逻辑...
    
            if (instance == null) {
                synchronized(Singleton4.class) {
                    if(instance == null) {
                        instance = new Singleton4();
                    }
                }
            }
            return instance;
        }
    }
    

    在这种实现中,我们在不锁方法的前提下先执行一些业务逻辑,然后,如果此时有两个线程同时判断了instance==null,只有一个线程能获取Singleton.class的锁,然后实例化instance对象,再然后另一个线程也获取到了锁,此时它第二次判断instance==nullfalse,就会直接返回上一个线程已经实例化的instance单例。这种方法被称为DCL(Double Check Lock,双重校验锁),基本达到了我们的需求。

    volatile

    那么,刚刚这种实现是不是就万无一失了呢?并不是。这里涉及到了一些更底层的知识:

    我们知道,所有编程语言最终都会转换成指令供CPU执行,例如在Java中创建一个对象Object o = new Object(),就至少包含下面3条CPU指令:

    1. 在内存中为该对象开辟一块空间,此时,该对象的状态称为“半初始化”,各成员的值都是默认值,例如int类型的默认值为0,引用类型的默认值为null
    2. 调用Object的构造方法,各成员初始化,如:int i = 1
    3. 将o的引用指向该对象

    而CPU为了运行效率,会对一些指令进行重排。例如第2步中Object初始化的操作可能会比较耗时,而它又对第3步没有影响,CPU就可能会先执行第3步,将o指向开辟好的内存区域,然后再初始化o。

    那么,这对我们的单例模式有什么影响呢?

    我们再来模拟一下两个线程同时调用getInstance()的场景:线程A获取Singleton4.class锁之后,初始化instance过程中,由于指令重排,先将instance的引用指向某块内存区域,然而尚未完成instance对象的初始化,instance处于一个半初始化的状态。此时线程B第二次判断instance时发现它不为null,就会直接返回这个instance对象,而这个instance中各个成员变量都尚未被赋值。

    在实际生产中,这样的问题发生的机率极低,但是一旦发生,就可能造成很大的损失并且难以排查。想要避免这种问题,关键就在于禁止CPU的“指令重排序”操作。而Java中提供了volatile关键字用于实现这一点,volatile的作用有两点:

    1. 保证内存可见性
    2. 禁止指令重排序

    简单介绍一下“保证内存可见性”:

    由于内存屏障的存在,线程操作某一个变量时,会先从主内存中获取一个该变量的副本存入自己的工作内存中,操作完后再写入主内存,而各个线程的工作内存之间是隔离的。volatile保证内存可见性的意思就是每当线程操作一个变量时,都会强制重新从主内存中读取,操作完存入主内存时,也会通知其他线程重新从主内存更新该变量。由于即时更新的原因,各个线程操作的变量可以看作不是缓存的副本,而是同一个,对变量的操作是彼此可见的,也就是“内存可见性”。

    在声明instance实例时加上volatile关键字,就可以避免上述的因指令重排所引发的问题。

    public class Singleton5 {
    
        private static volatile Singleton5 instance;
    
        private Singleton5() {}
    
        public static Singleton5 getInstance() {
            if (instance == null) {
                synchronized(Singleton5.class) {
                    if(instance == null) {
                        instance = new Singleton5();
                    }
                }
            }
            return instance;
        }
    }
    

    这就是最终版的DCL,实现了懒加载、并发安全等一系列要求。

    其他方式

    除了上述方法外,Java中的单例模式还能通过静态内部类、内部枚举类来实现,事实上,使用枚举实现单例模式是《Effective Java》一书中最为推荐的方式,它不仅代码简洁,并且与DCL方式相比,它还能抵御基于反射的对单例模式的破坏。

    public enum  EnumSingleton {
        INSTANCE;
        public EnumSingleton getInstance() {
            return INSTANCE;
        }
    }
    

    枚举方式在底层已经为我们实现了并发情况下的安全检查,并且通过反射创建对象时,由于该类是枚举类,会直接抛出异常。

    单元素的枚举类型已经成为实现Singleton的最佳方法

  • 相关阅读:
    浅谈通信网络(五)——TCP层
    浅谈通信网络(四)——报文转发(IP/MAC)
    浅谈通信网络(三)——TCP/IP协议
    《linux内核设计与实现》阅读笔记-进程与调度
    深入理解计算机系统 BombLab 实验报告
    汇编语言十二
    汇编语言十一
    汇编语言实验十
    汇编语言实验九
    汇编语言实验七
  • 原文地址:https://www.cnblogs.com/2511zzZ/p/12812258.html
Copyright © 2020-2023  润新知