• 手撕设计模式-单例模式详细总结和常见代码踩坑


    关注我的公众号:帅哥趣谈,文章抢先看。

    按照惯例,先来一波一键三连。

    前言 

    大家好,帅哥趣谈本期跟大家聊一下设计模式之单例模式。单例模式和工厂模式在23种设计模式中排名最靠前的两位,相交其他模式,大部分程序员在实际编程过程中都会接触到,是绕不开的。比如,大家肯定写过的工具类、线程池、定时任务等,一般都会封装成单例。考虑到多线程,自然就想到了线程安全,所以会写出线程安全的单例代码才是程序员必须要掌握的。(每个同学都必须要会工厂模式和单例模式,是必备技能包)

    概念定义 

    定义:确保一个类最多只有一个实例,并提供一个全局访问点。

    大家都知道构建一个对象的实例是通过 new 执行构造方法创建,如果构造方法谁都可以访问,那么就不能保证唯一,所以单例模式对类的改造如下。

    第一:要把构造方法私有化。

    第二:类本身要对外提供访问入口,全局唯一,自然想到的就是静态方法static修饰的方法,具体如何使用,请往下看。

    上面是通过类定义的方式来阐述单例模式的定义,但是生成对象实例还有一个方法,就是Java反射机制来构造对象,这样会绕过类定义的方式,全局唯一性会遭到破坏,于是就有了枚举类的方式,这种方式Java反射不能侵入(Java语言设计规范的原因)

    学习重点

    • 常见的几种写法:饿汉式、懒汉式、锁代码块、双重验证式、枚举方式
    • 预加载和懒加载的优缺点
    • Java指令重排引起的问题及解决办法
    • 为什么会出现线程安全问题,如何保证
    • 如何防止Java反射的破坏性
    • 最终推荐使用的方式是什么?(学习完,希望读者能够识别出来)

    接下来就让我们带着问题去探索单例模式的前世今生。

    学习方法提炼:在学习某个知识点的时候,大家一定要弄明白该知识点的重点是什么,这样就能找到更精准的学习资料,也能做到重要信息和次要的筛选,用最高效的方法快速达到学会的目的。

    (这个学习方法是我多年经验的总结,分享出来大家一起共勉,千万不要眉毛胡子一把抓,到头什么收货都没有)

    (知识点的总结有一个特点,就是一次完整总结,终身受益。正好跟Java的特性,一次编写,到处运行 Write once,Run anywhere 遥相呼应。如果这块不重视,你就会发现,你的整个职业生涯都是在重复整理这些知识点,每次效果都不好,不断的恶性循环,这是很多程序员不能突破瓶颈的根本原因。)

    加载方式

    • 预加载:顾名思义,就是预先加载。再进一步解释就是还没有使用该单例对象,但是,该单例对象就已经被加载到内存了。如果长期不使用,会造成内存浪费。
    • 懒加载:为了避免内存的浪费,我们可以采用懒加载,即用到该单例对象的时候再创建。

    线程安全

    创建对象分三个步骤:①初始化内存空间、②初始化对象、③设置instance指向刚分配的内存地址。

    1. memory=allocate();//1:初始化内存空间
    2. ctorInstance(memory);//2:初始化对象
    3. instance=memory();//3:设置instance指向刚分配的内存地址

    在不加锁的前提条件下,采用懒加载方式时,会存在多个请求同时访问的情况出现,第一次实例化对象的时候会出现多线程同步的问题,再加上Java本身会对指令执行的顺序重排优化,这就造成了乱序的问题,多个请求之间会彼此覆盖,所以会得到多个对象实例,或者拿到未初始化完成的对象出现异常,这违背了单例的设计初衷。系统设计就要求单模式必须是线程安全的,对象实例有且只有一次初始化。保证线程安全,第一要使用synchronized加锁,第二要使用volatile保证对象实例化过程的顺序性(防止指令重排,保证执行顺序严格按照步骤①②③执行)。 

    具体实现

    饿汉式

    最简单的一种实现,在类装载过程中,完成实例化,避免多线程问题。

    1. public class EagerSingleton {
    2. private static final EagerSingleton INSTANCE = new EagerSingleton();
    3. private EagerSingleton() {
    4. }
    5. public static EagerSingleton getInstance() {
    6. return INSTANCE;
    7. }
    8. }

    利用静态内部类的方式,将对象实例化延后。(预加载改成懒加载)(内部类不了解原理的一定要慎用,否则是给自己下毒,评论区讨论下是否给自己下过绊子) 

    1. public class EagerSingleton {
    2. private static EagerSingleton INSTANCE = null;
    3. static {
    4. INSTANCE = new EagerSingleton();
    5. }
    6. private EagerSingleton() {
    7. }
    8. public static EagerSingleton getInstance() {
    9. return INSTANCE;
    10. }
    11. }

    懒汉式

    针对饿汉式而命名,在使用的时候完成初始化,相比较而言更灵活。但是坑也比较多,下面先看常见的坑。

    错误写法(一)

    在初始化的时候,存在多线程间覆盖。具体是,两个线程同时执行第8行代码,对象实例都是空,分别执行第9行,那么后边执行的就会覆盖前面执行,造成了资源的浪费。

    1. public class LazySingleton {
    2. private static LazySingleton INSTANCE = null;
    3. private LazySingleton() {
    4. }
    5. public static LazySingleton getInstance() {
    6. if (INSTANCE == null) {
    7. INSTANCE = new LazySingleton();
    8. }
    9. return INSTANCE;
    10. }
    11. }

    错误写法(二)

    锁的错误使用,这也解决了多线程的问题,但是也引入了新的性能问题:太慢。synchronized把整个方法包起来,也就是每个线程进入的时候,都需要等待其他线程结束调用,才能拿到实例,在性能敏感的场景,是比较致命的。

    1. public class LazySingleton {
    2. private static LazySingleton INSTANCE = null;
    3. private LazySingleton() {
    4. }
    5. public static synchronized LazySingleton getInstance() {
    6. if (INSTANCE == null) {
    7. INSTANCE = new LazySingleton();
    8. }
    9. return INSTANCE;
    10. }
    11. }

    错误写法(三)

    虽然有一步判断是否为空,但是并没有卵用,这种写法跟第一种情况相同,对象仍然会被实例化多次。比如两个线程同时执行第8行代码,线程A获取锁执行new操作,线程B等待获取锁执行new操作,当线程B拿到锁之后就会覆盖线程A的操作。

    1. public class LazySingleton {
    2. private static LazySingleton INSTANCE = null;
    3. private LazySingleton() {
    4. }
    5. public static LazySingleton getInstance() {
    6. if (INSTANCE == null) {
    7. synchronized (LazySingleton.class) {
    8. INSTANCE = new LazySingleton();
    9. }
    10. }
    11. return INSTANCE;
    12. }
    13. }

    错误写法(四)

    这种双重判断机制保证了单例对象只初始化一次,但是会面临指令重排导致的系列问题。(看上面解释)

    1. public class LazySingleton {
    2. private static LazySingleton INSTANCE = null;
    3. private LazySingleton() {
    4. }
    5. public static LazySingleton getInstance() {
    6. if (INSTANCE == null) {
    7. synchronized (LazySingleton.class) {
    8. if (INSTANCE == null) {
    9. INSTANCE = new LazySingleton();
    10. }
    11. }
    12. }
    13. return INSTANCE;
    14. }
    15. }

    正确写法

    双重检查+阻止指令重排序。这根错误四的写法仅仅有一个volatile关键字之差,但是效果完全不同。

    1. public class LazySingleton {
    2. private static volatile LazySingleton INSTANCE = null;
    3. private LazySingleton() {
    4. }
    5. public static LazySingleton getInstance() {
    6. if (INSTANCE == null) {
    7. synchronized (LazySingleton.class) {
    8. if (INSTANCE == null) {
    9. INSTANCE = new LazySingleton();
    10. }
    11. }
    12. }
    13. return INSTANCE;
    14. }
    15. }

    枚举式

    枚举式的出现是为了解决通过反序列化或反射实例化对象的问题。枚举的反序列化是通过name查找对象,不会产生新的对象;根据JVM规范,通过反射创建枚举对象时,会抛出IllegalArgumentException异常,这样相当于通过语法糖防止反序列化和反射破坏单例。

    1. public enum EnumSingleton {
    2. INSTANCE;
    3. public void method1() {
    4. // do something
    5. }
    6. public Object method2() {
    7. // do something and return something else
    8. return new Object();
    9. }
    10. }

    总结

    大力推荐枚举式,其次是懒汉式的双重检查+阻止指令重排序(volatile)

    实现方式加载方式是否线程安全优点缺点优化
    饿汉式预加载类装载过程就完成实例化1.如果整个生命周期没有使用,造成内存资源浪费
    2.构造函数不支持参数传递,不能满足个性化定制的需要
    针对第一个缺点,可以借助静态内部类的方式,将对象实例化的时间延后
    懒汉式懒加载灵活使用时可个性化定制如果对多线程和指令重排序不了解,容易写出错误代码
    枚举式懒加载实现简单,且绝对安全
  • 相关阅读:
    openGauss学习笔记-82 openGauss 数据库管理-内存优化表MOT管理-内存表特性-MOT使用准备前提条件
    uboot 顶层Makefile-make xxx_deconfig过程说明三
    如何排版一篇优秀的公众号文章呢?
    【运维篇】Redis 性能测试工具实践
    竞赛选题 深度学习实现行人重识别 - python opencv yolo Reid
    智能电销机器人,主要体现的价值是什么
    达梦数据备份还原(物理逻辑)
    110-注解JSONField、DateTimeFormat、JsonFormat、JsonProperty
    FPGA project : flash_erasure
    企业工程项目管理系统源码(三控:进度组织、质量安全、预算资金成本、二平台:招采、设计管理)
  • 原文地址:https://blog.csdn.net/shishuai4206/article/details/126524696