初探设计模式(二)开篇:单例模式怎么用,你学会了吗?

143 阅读6分钟

UML类图熟悉后,设计模式算是正式开篇了,下面我想先说下为啥要写设计模式专栏。

其实,对于一个已从业三年、还在持续打怪的同学,最普遍的建议是,学习重点应该放在代码重构、架构设计等方面。我认可这种说法,因为确实有一定的经验需要进阶了嘛。但同时,我也觉得不那么绝对,尤其最近在回顾到一些设计模式的书时,想做总结的想法愈发强烈。

所以我梳理了此次开设专栏的目的:

一、前几年自己忙于业务开发,代码基础和写作能力都是马马虎虎,想借此机会做些梳理、总结和提升;

二、最近重读设计模式,发现其实大部分设计模式在生产环境中使用的很少,所以很多人是看完就忘。我想,或许是我没有使用过,或许有些设计模式确实没有实战作用,总之,挖一下常见设计模式的应用场景,帮助自己、也帮助学习设计模式的同学做下总结。

网上讲解设计模式的文章其实很多,我只想从自己的观点出发去和大家聊一聊。

单例模式是什么

单例模式(Singleton Pattern),属于创建型模式的一种。使用单例模式可以确保在一个进程中,某个类只有一个实例,并提供一个全局访问点

常见的UML类图如下:(工具:Mac版draw.io,UML图如有疑问,传送门:初探设计模式(一)10分钟速成UML类图

1.png

为什么要有单例模式

单例模式的优点是什么?

  • 保证唯一:有些类在业务概念上就是唯一的(比如唯一ID生成器、配置信息、连接池),适合设计为单例类;
  • 提升性能:对象的创建需要消耗资源,有些对象创建时消耗资源较多(比如访问IO、数据库),当需要频繁创建、销毁时,为了提升性能,使用单例就比较合适

单例模式有缺点吗?

最大的缺点在于扩展性,单例模式非面向接口编程,违背了依赖倒置原则。举个例子,如果针对不同的场景(比如生成订单号、退款单号)需要采用不同的ID生成器,就不太好支持了。

怎么实现单例模式

总结

实现方式是否延迟加载是否线程安全使用情况
饿汉式最简单,常用
懒汉式线程不安全,几乎不用
synchronized懒汉式并发度太低,几乎不用
双重校验锁DCL常用,注意对变量加volatile
静态内部类常用
枚举不常用,但《Effective Java》推荐使用

标准代码怎么写

手写一个单例,这是面试官最常问的一个问题。那么,我们来看下几种常见的实现方式。 注意几点:

  1. 私有构造器,避免外部通过new创建实例
  2. 考虑加载方式:预加载/懒加载(延迟加载)
  3. 考虑创建对象时的线程安全问题
  4. 考虑获取对象时的并发性能

饿汉式:不管用不用,先初始化完成(预加载)

顾名思义,不管三七二十一,上来先创建好。即类加载的时候就把静态实例创建、初始化完成

特点:线程安全,但对资源有浪费(即使没有用,也做了初始化)

public class Singleton {
    /**
     * 【类加载】时就把静态实例初始化完成
     */
    private static final Singleton INSTANCE = new Singleton();

    /**
     * 私有的构造方法
     */
    private Singleton() {
    }

    /**
     * 公开的全局访问点
     */
    public static Singleton getInstance() {
        return INSTANCE;
    }
}

懒汉式:用的时候再做初始化(懒加载)

特点:懒加载,解决了饿汉式资源浪费的问题;但每次获取实例时都要获取同步锁(线程安全的懒汉式),会导致性能下降,不支持高并发(并发度实际上只有1,相当于串行操作)。

线程不安全的懒汉式

这种写法,多线程并发调用getInstance()获取单例时,可能会创建多个实例:

  • 线程A执行到对象初始化instance=new Singleton();还没获得实例
  • 线程B执行到if时,就会判断为true,也进入对象初始化代码
public class Singleton {
    private static Singleton instance = null;

    private Singleton() {
    }

    public static Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}

优化:线程安全的懒汉式

对公开的全局访问点方法加synchnorized锁即可。

public class Singleton {
    private static Singleton instance = null;

    private Singleton() {
    }

    public static synchronized Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}

双重校验锁(DCL,Double Checked Locking)

特点:针对懒汉式加锁的性能问题做了优化,只有对象为空时,才去获取同步锁。

未使用volatile的双重校验锁

public class Singleton {
    private static Singleton instance = null;

    private Singleton() {
    }
    
    public static Singleton getInstance() {
        if (instance == null) {             // Single Checked
            synchronized(Singleton.class) { // 类级别锁
                if (instance == null) {     // Double Checked
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

待修改,单写一篇博客整理下。 传送门 The "Double-Checked Locking is Broken" Declaration

由于JVM的指令重排序优化,这种方式可能导致出错。因为instance = new Singleton();不是原子操作:

  1. 给instance分配内存;(instance=null)
  2. 调用构造函数,初始化成员变量(instance=null)
  3. 将instance实例指向分配的内存空间(instance!=null) 我们预期的执行顺序是1-2-3,实际执行可能被优化为1-3-2,当按照1-3-2顺序执行时,如果执行完3,另一个线程针对Single Check的校验结果就会返回true,但实际上intance还未被初始化,执行出错。

下图是Singleton instance = new Singleton();这行代码对应的Java字节码(Java8):

  • new: 创建一个对象,并将其引用值压入栈顶;
  • dup: 复制栈顶一个字长的数据,将复制后的数据压栈;
  • invokespecial: 调用构造器进行对象的初始化,这一步会消耗掉操作数栈顶的引用(上一步dup出来的数据)作为传给构造器的“this”参数
  • astore: 把操作数栈顶的引用消耗掉,保存到指定的局部变量Singleton instance去 image.png

优化:使用volatile的双重校验锁

给instance变量增加volatile关键字修饰。volatile可禁止指令重排序优化。

public class Singleton {
    // volatile关键字修饰,禁止指令重排序优化
    private volatile static Singleton instance = null;

    private Singleton() {
    }
    
    public static Singleton getInstance() {
        if (instance == null) {             // Single Checked
            synchronized(Singleton.class) {
                if (instance == null) {     // Double Checked
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

静态内部类

个人理解,这是静态内部类+饿汉式,只要不使用内部类,JVM就不会去加载这个单例类。

特点:懒加载,线程安全。

public class Singleton {
    private Singleton() {
    }
    
    private static class SingletonHolder {
        private static final Singleton INSTANCE = new Singleton();
    }
    
    public static Singleton getInstance() {
        return SingletonHolder.INSTANCE;
    }
}

枚举单例

public enum Singleton {
     // 类似 public static final Singleton INSTANCE;
     INSTANCE;
 }

生产环境怎么用

使用建议

  • 简单,不需要节约资源:饿汉式
  • 需要考虑资源(延迟加载):
    • 双重校验锁单例
    • 静态内部类单例
    • 枚举单例

使用场景

  1. ID生成器
  2. 数据/资源管理器等
  3. Spring默认采用单例模式来管理Bean对象