• 设计模式之单例模式


    聊聊单例模式,面试加分题

    犹记得之前面xx时,面试官一上来就问你知道哪些设计模式,来手写一个单例模式的场景;尴尬的我,只写了懒汉式饿汉式,对于单例其他的变种一概不知;这次就来弥补下这方面的知识盲区!

    饿汉式

    饿汉式,从字面上理解就是很饿,一上来就要吃的,那么它会把吃的先准备好,以满足它的需求;那么对应到程序上的表现就为:在类加载的时候就会首先进行实例的初始化,后面如果应用程序需要这个实例的话,就有现成的了,可以直接使用当前的单例对象!

    我们来手写下饿汉式的代码:

    public class Singleton{
        // 声明静态私有实例 并实例化
        private static Singleton singleton = new Singleton();
    
        // 提供对外初始化方法 静态类加载就初始化
        public static Singleton initInstance(){
            return singleton;
        }
    
        // 声明私有构造方法  即在外部类无法通过new 初始化实例
        private Singleton(){
    
        }
    
        public void doSomeThing(){
            System.out.println("do some thing!");
        }
    }
    class SingletonDemo{
        public static void main(String[] args) {
            Singleton singleton = Singleton.initInstance();
        }
    }
    

    饿汉式的优点:它是线程安全的,因为单例对象在类加载的时候就被初始化了,当调用单例对象时只需要去把对应的对象赋值给变量即可!

    饿汉式的缺点:如果这个类不经常使用,会造成一定的资源浪费!

    懒汉式

    懒汉式,就是比较懒,每次需要填饱肚子时才会外出觅食;那么对应到程序层面的理解:当应用程序需要某个对象时,该对象的类就会去创建一个实例,而不是提前准备好的!

    我们来手写下懒汉式的代码:

    public class Singleton2 {
        // 声明私有静态对象
        private static Singleton2 singleton2;
    
        // 对外提供初始化方法
        public static Singleton2 initInstance(){
            if(singleton2 == null){
                singleton2 = new Singleton2();
            }
            return singleton2;
        }
    
        // 私有构造器
        private Singleton2(){
    
        }
    
        public void doSomeThing(){
            System.out.println("do some thing!");
        }
    }
    class SingletonDemo2{
        public static void main(String[] args) {
            Singleton2 singleton2 = Singleton2.initInstance();
            singleton2.doSomeThing();
        }
    }
    

    同样我们看下懒汉式的优点:不会造成资源的浪费

    懒汉式的缺点:多线程情况下,会有线程安全的问题;

    上面我们可以看到,饿汉式和懒汉式的唯一区别就是:饿汉式在类加载时就完成了对象的初始化,而懒汉式是在需要初始化的时候再去初始化对象;其实在单线程情况下,他们都是线程安全的;但是我们写的代码,必须考虑多线程情况下的并发问题,那么懒汉式的这种写法基本不满足需求,我们需要做些改造,使得它变得线程安全,满足我们的需求!

    双重检测锁

    我们知道,懒汉式下对象的初始化在并发环境下,可能多个线程同时执行到singleton2 == null,从而初始化了多个实例,这就引发了线程安全问题!

    我们就需要改写它的初始化方法,我们知道加锁可以解决一般的线程安全问题,synchronized这个关键字可以修饰一个代码块或方法,被其修饰的方法或代码块就被加了锁;而从某些方面理解,synchronized是个同步锁,亦是个可重入锁!哈哈,关于锁的种类及概念有点多,后面准备写一篇关于锁的博客来总结下;不再发散了,回归正题

    我们来改造下懒汉式的初始化方法如下:

    // 对外提供初始化方法
    public synchronized static Singleton2 initInstance(){
        if(singleton2 == null){
            singleton2 = new Singleton2();
        }
        return singleton2;
    }
    

    我们看下上面的代码,初看没什么问题是解决了线程安全问题;但是由于整个方法都被synchronized修饰,那么在多线程的情况下就增加了线程同步的开销,降低了程序的执行效率;为了改进这个问题,我们将synchronized放入到方法内,实现代码块的同步;改下如下:

    // 对外提供初始化方法
    public  static Singleton2 initInstance(){
        if(singleton2 == null){
            synchronized(Singleton2.class){
                singleton2 = new Singleton2();
            }
        }
        return singleton2;
    }
    

    呃,这样就满足了我们的要求了吗?聪明如你一定发现了,虽然我们将synchronized移到了方法内部,降低了同步的开销,但是在并发的情况下假设多个线程同时执行到if(singleton2 == null)时,依旧会排队初始化Singleton2实例,这样又会造成新的线程安全问题;那么为了解决这个问题,就出现了大名鼎鼎的“双重检测锁”。我们来看下它的实现,将上述代码改写如下:

    // 对外提供初始化方法
    public  static Singleton2 initInstance(){
        if(singleton2 == null){// 第一次非空判断
            synchronized(Singleton2.class){
                if(singleton2 == null)// 第二次非空判断
                    singleton2 = new Singleton2();
            }
        }
        return singleton2;
    }
    

    哈哈,这个双重即是判断两次的意思,并不是加两把锁哈;那么这样就能行了吗?初看没问题啊,但是我们细想之下这样写真的没问题吗?你写的代码,执行的时候真的会按你想的过程执行吗?有没有考虑过指令重排呢?问题就出现在new Singleton2()这个代码上,这行代码不是一个原子操作!

    我们再来回顾下指令重排的大致执行流程:

    1.给对象实例分配内存空间

    2.调用对象构造方法,初始化成员变量

    3.将构造的对象指向分配的内存空间

    问题就出在指令重排后,cpu对指令重排的优化上,也就是说上述的三个过程并不是每次都是1-2-3顺序执行的,而是也有可能1-3-2;那么我们试想下并发情况下可能出现的场景,当线程A执行到步骤3时,cpu时间片正好轮询到线程B,那么线程B判断实例已经指向了对应的内存空间,不为null就不会 初始化实例了,就得到了一个未初始化完成的对象,这就导致了问题的诞生!

    为了解决这个问题,我们知道还有一个关键字volatile可以完美的解决指令重排,使得非原子性的操作对其他对象是可见的!(volatile关键字保障了变量的内存的可见性和一致性问题,关于内存屏障可以看我之前的一篇文章JMM 内存模型知识点探究了解)。那么我们将懒汉式改写如下:

    public class Singleton2 {
        // 声明私有静态对象
        private volatile static Singleton2 singleton2;
    
        // 对外提供初始化方法
        public  static Singleton2 initInstance(){
            if(singleton2 == null){
                synchronized(Singleton2.class){
                    if(singleton2 == null)
                        singleton2 = new Singleton2();
                }
            }
            return singleton2;
        }
    
        // 私有构造器
        private Singleton2(){
    
        }
    
        public void doSomeThing(){
            System.out.println("do some thing!");
        }
    }
    class SingletonDemo2{
        public static void main(String[] args) {
            Singleton2 singleton2 = Singleton2.initInstance();
            singleton2.doSomeThing();
        }
    }
    

    其实除了上面的单例实现外,还有两种常见的单例实现

    静态内部类

    代码如下:

    public class InnerClassSingleton {
        // 私有静态内部类
        private static class InnerInstance{
            private static final InnerClassSingleton singleton = new InnerClassSingleton();
        }
        // 对外提供的初始化方法
        public static InnerClassSingleton initInstance(){
            return InnerInstance.singleton;
        }
        // 私有构造器
        private InnerClassSingleton(){
    
        }
    
        public void doSomeThing(){
            System.out.println("do some thing!");
        }
    }
    class InnerClassSingletonDemo{
        public static void main(String[] args) {
            InnerClassSingleton innerClassSingleton = InnerClassSingleton.initInstance();
            innerClassSingleton.doSomeThing();
        }
    }
    

    其实,静态内部类的方式和饿汉式本质是一样的,都是根据类加载机制来初始化实例,从而保证单例和线程安全的;不同的是静态内部类的方式是按需构建实例,不会如饿汉式一样造成资源浪费的问题;所以这个是饿汉式一个比较好的变种!

    枚举类

    枚举是比较推荐的一种单例模式,它是线程安全的,且通过反射、序列化以及反序列化都无法破坏它的单例属性(其他的单例采用私有构造器的实现其实并不安全),至于为什么呢?这个可以参考博客:[为什么要用枚举实现单例模式(避免反射、序列化问题)]

    代码如下:

    public class EnumSingleton {
        // 声明私有的枚举类型
        private enum Enum{
            INSTANCE;
            // 声明单例对象
            private final EnumSingleton instance;
            // 实例化
            Enum(){
                instance = new EnumSingleton();
            }
            private EnumSingleton getInstance(){
                return instance;
            }
        }
        // 对外提供的初始化方法
        public static EnumSingleton initInstance(){
            return Enum.INSTANCE.getInstance();
        }
    
        // 私有构造器
        private EnumSingleton(){
    
        }
    
        public void doSomeThing(){
            System.out.println("do some thing!");
        }
    }
    class EnumSingletonDemo{
        public static void main(String[] args) {
            EnumSingleton enumSingleton = EnumSingleton.initInstance();
            enumSingleton.doSomeThing();
        }
    }
    

    好,至此我们总结了单例的几种实现方式;比较推荐的是后面两种方式,一般懒汉式我们就采用双重检测锁的方式;你可以发散思考下单例的应用场景,例如Spring中的Bean的初始化就是单例模式的典型应用,或者在消息中心中使用比较频繁的短链接!

    余路那么长,还是得带着虔诚上路...
  • 相关阅读:
    debug
    whlie and for
    while and for 2
    用鸿蒙开发AI应用(七)触摸屏控制LED
    animation动画组件在鸿蒙中的应用&鸿蒙的定时函数和js的动画效果
    2020技术征文大赛获奖名单公示
    HarmonyOS三方件开发指南(8)——RoundedImage
    从微信小程序到鸿蒙js开发【02】——数据绑定&tabBar&swiper
    从微信小程序到鸿蒙js开发【01】——环境搭建&flex布局
    HarmonyOS三方件开发指南(7)——compress组件
  • 原文地址:https://www.cnblogs.com/itiaotiao/p/13457144.html
Copyright © 2020-2023  润新知