zoukankan      html  css  js  c++  java
  • 「补课」进行时:设计模式(1)——人人都能应该懂的单例模式

    1. 引言

    最近在看秦小波老师的《设计模式之禅》这本书,里面有句话对我触动挺大的。

    设计模式已经诞近 20 年了,其间出版了很多关于它的经典著作,相信大家都能如数家珍。尽管有这么多书,工作 5 年了还不知道什么是策略模式、状态模式、责任链模式的程序员大有人在。

    很不幸,我就是这部分人当中的一个。回想起这几年的工作生涯,设计模式不能说没有接触过,但是绝对不多,能想到的随手写出来的几个设计模式也仅限于「单例模式」、「工厂模式」、「建造者模式」、「代理模式」、「装饰模式」。

    好吧,我认知比较深的也就这几个模式,说出来都自己感觉脸红,还有很大一部分仅限于听过,说了以后大致知道是什么玩意,没有细细的研究过,正好趁着这个机会,写点文章,给自己补补课,所以这个系列的名字叫「补课」进行时。

    至于为什么要选设计模式,因为设计模式这个东西,它是软件行业的经验总结,因此它具有更广泛的适应性,不管你使用什么编程语言,不管你遇到什么业务类型,都需要用到它。

    因为它是一个指导思想,学习了它以后,我们可以站在一个更高的层次去赏析程序代码、软件设计、架构,完成一个 Coder 的蜕变。

    2. 单例模式

    在古代行军打仗的时候,每支军队都要有一个将军,战场上如何作战,完全需要听将军的指挥,将军怎么说,这个仗就怎么打,每个士兵都知道将军是谁,而不需要在将军前面加上张将军或者是李将军。

    既然将军只能有一个,我们需要用程序去实现这个将军的话,也就是一个类只能产生一个将军的对象,不能产生多个,这就是单例模式的要义。

    产生一个对象有多重方式,最常见的是直接 new 一个出来,当然,还可以有反射、复制等操作,我们如何来控制一个类只能产生一个对象呢?

    最简单的做法是直接在构造函数上动手脚,使用 new 来新建对象的时候,会根据输入的参数调用相应的构造函数,我们如果直接把构造函数设置成 private ,这样就可以做到不允许外部类来访问创建对象,从而保证对象的唯一性。

    public class General {
        // 初始化一个将军
        private static final General general = new General();
    
        // 构造函数私有化
        private General() {
    
        }
    
        public static General getInstance() {
            return general;
        }
    
        public void command() {
            System.out.println("将军下令,兄弟们跟我上啊!!!");
        }
    }
    

    现在我们有了一个将军类,接下来我们实现一个士兵类:

    public class Soldier {
        public static void main(String[] args) {
            for (int soldiers = 0; soldiers < 5; soldiers++) {
                General general = General.getInstance();
                general.command();
            }
        }
    }
    

    有 5 个士兵收到了将军的命令,跟着将军一起冲锋陷阵,成就一世英名。

    单例模式(Singleton Pattern)的定义异常简单:Ensure a class has only one instance, and provide a global point of accessto it.(确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例。)

    优点:

    由于单例模式在内存中只有一个实例,减少了内存开支,特别是一个对象需要频繁地创建、销毁时,而且创建或销毁时性能又无法优化,单例模式的优势就非常明显。

    缺点:

    单例模式一般没有接口,扩展很困难,若要扩展,除了修改代码基本上没有第二种途径可以实现。

    注意事项:

    在某些有一定并发的场景中,需要注意线程同步的问题,防止创建多个对象,造成未知错误异常。

    因为单例模式有多种变形的写法,一定要注意这个问题,举一个会产生线程同步问题的例子:

    public class Singleton {
        private static Singleton singleton = null;
    
        private Singleton() {
        }
    
        public static Singleton getInstance() {
            if (singleton == null) {
                singleton = new Singleton();
            }
            return singleton;
        }
    }
    

    这种方案在没有并发的情况下不会出现任何问题,但若是出现了并发,就会在内存中产生多个实例。

    原因是线程 A 在执行到 singleton = new Singleton() 这句话的时候,但是还没有完成实例的初始化操作,线程 B 恰巧执行到了 singleton == null 的判断,这时,线程 B 判断条件为真,也去执行 singleton 初始化的这句代码,就会造成线程 A 获得了一个对象,线程 B 也获得了一个对象。

    解决线程不安全的方式有很多种,比如加一个 synchronized 关键字。

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

    这种在代码块中使用 synchronized 关键字的方式名字叫做懒汉式单例,前面我们写的那个将军叫做饿汉式单例。

    饿汉式和懒汉式的命名很有意思:

    • 饿汉:类一旦加载,就把单例初始化完成,保证 getInstance 的时候,单例是已经存在的了。
    • 懒汉:懒汉比较懒,只有当调用getInstance的时候,才回去初始化这个单例。

    饿汉式天生就是线程安全的,可以直接用于多线程而不会出现问题,懒汉式本身是非线程安全的,为了实现线程安全有几种写法,上面那种加方法锁的方式有点笨重,我们还可以使用同步代码块,减少锁的颗粒大小。

    public class Singleton2 {
        private static volatile Singleton2 singleton2;
    
        private Singleton2() {
        }
    
        public static synchronized Singleton2 getInstance() {
            // 第一层检查,检查是否有引用指向对象,高并发情况下会有多个线程同时进入
            if(singleton2 == null) {
                // 第一层锁,保证只有一个线程进入
                synchronized (Singleton2.class) {
                    // 第二层检查
                    if (singleton2 == null) {
                        // volatile 关键字作用为禁止指令重排,保证返回 Singleton 对象一定在创建对象后
                        singleton2 = new Singleton2();
                    }
                }
            }
            return singleton2;
        }
    }
    

    关于 volatile 关键字多说两句,如果对象没有 volatile 关键字,这里会涉及到一个指令重排序问题, singleton2 = new Singleton2() 这句话实际上会涉及到以下三件事儿:

    1. 申请一块内存空间。

    2. 在这块空间里实例化对象。

    3. singleton2 的引用指向这块空间地址。

    对于以上步骤,指令重排序很有可能不是按上面 123 步骤依次执行的。比如,先执行 1 申请一块内存空间,然后执行 3 步骤, singleton2 的引用去指向刚刚申请的内存空间地址,那么,当它再去执行 2 步骤,判断 singleton2 时,由于 singleton2 已经指向了某一地址,它就不会再为 null 了,因此,也就不会实例化对象了。

    而我们添加的关键字 volatile 就是为了解决这个问题,因为 volatile 可以禁止指令重排序。

    不过还是建议大家使用饿汉式的单例模式,毕竟比较简单,出错的概率比较低。

    2.1 单例模式扩展——上限的多例模式

    还是刚才那个例子,如果一只军队中,偶然情况下出现了 3 个将军,士兵需要听从这 3 个将军的命令,我们用代码实现一下,这段代码稍微有点长:

    public class General1 {
        // 定义最多能产生的将军数量
        private static int maxNumOfGeneral1 = 3;
    
        // 定义一个列表,存放所有将军的名字
        private static ArrayList<String> nameList = new ArrayList<> ();
    
        // 定义一个列表,容纳所有的将军实例
        private static ArrayList<General1> general1ArrayList = new ArrayList<> ();
    
        // 定义当前将军序号
        private static int countNumOfGeneral1 = 0;
    
        // 在静态代码块中产生所有的将军
        static {
            for (int i = 0; i < maxNumOfGeneral1; i++) {
                general1ArrayList.add(new General1(String.valueOf(i)));
            }
        }
    
        private General1() {
            // 目的是不产生将军
        }
    
        private General1(String name) {
            // 给将军加个名字,建立一个将军对象
            nameList.add(name);
        }
    
        public static General1 getInstance() {
            // 随机产生一个将军,只要能发号施令就成
            Random random = new Random();
            countNumOfGeneral1 = random.nextInt(maxNumOfGeneral1);
            return general1ArrayList.get(countNumOfGeneral1);
        }
    
        public void command() {
            System.out.println("将军说:我是 " + nameList.get(countNumOfGeneral1) + " 号将军");
        }
    }
    

    上面这段代码使用了两个 ArrayList 分别存储实例和实例变量。

    如果考虑到线程安全的问题,可以使用 Vector 来代替,或者加锁等方式。

    我们再创建一个士兵类,等将军发号施令:

    public class Soldier1 {
        public static void main(String[] args) {
            for (int soldiers1 = 0; soldiers1 < 5; soldiers1++) {
                General1 general = General1.getInstance();
                general.command();
            }
        }
    }
    

    结果是这样的:

    将军说:我是 0 号将军
    将军说:我是 0 号将军
    将军说:我是 1 号将军
    将军说:我是 0 号将军
    将军说:我是 2 号将军
    

    这种需要产生固定数量对象的模式就叫做有上限的多例模式,它是单例模式的一种扩展,采用有上限的多例模式,我们可以在设计时决定在内存中有多少个实例,方便系统进行扩展,修正单例可能存在的性能问题,提供系统的响应速度。例如读取文件,我们可以在系统启动时完成初始化工作,在内存中启动固定数量的 reader 实例,然后在需要读取文件时就可以快速响应。

  • 相关阅读:
    springboot之mybatis别名的设置
    webstorm
    万字长文把 VSCode 打造成 C++ 开发利器
    残差residual VS 误差 error
    参数与非参数的机器学习算法
    阿里云产品梳理
    aws产品整理
    Azure产品整理
    OpenStack产品摘要
    头条、美团、滴滴、阿里、腾讯、百度、华为、京东职级体系及对应薪酬
  • 原文地址:https://www.cnblogs.com/babycomeon/p/13845788.html
Copyright © 2011-2022 走看看