设计模式之单例模式

1、什么是单例模式

​ 单例模式是指保证 某个类在整个软件系统中只有一个对象实例,并且该类仅提供一个返回其对象实例的方法(通常为静态方法)

2、单例模式的种类

​ 经典的单例模式实现方式一般有五种:

2.1 饿汉式

// ①饿汉式:使用静态常量
static class Singleton {
    // 1.构造器私有化,其他类不能new
    private Singleton() {}
    // 2.类的内部创建对象
    private final static Singleton instance = new Singleton();
    // 3.向外部暴露一个静态的公共方法
    public static Singleton getInstance() {
        return instance;
    }
}
// ②饿汉式:使用静态代码块
static class Singleton {
    // 1.构造器私有化,其他类不能new
    private Singleton() {}
    private static final Singleton instance;
    // 2.静态代码块实例化
    static {
        instance = new Singleton();
    }
    // 3.向外部暴露一个静态的公共方法
    public static Singleton getInstance() {
        return instance;
    }
}

&#x997F;&#x6C49;&#x5F0F;顾名思义就是 迫不及待地加载该类的对象实例,对象实例的加载最早是在 类的加载过程中的初始化阶段(即静态引用变量的加载,对应字节码文件中 <clinit></clinit>方法的执行),加载过程由JVM保证线程安全。 &#x997F;&#x6C49;&#x5F0F;会浪费内存,但是随着计算机的发展,内存已经不是问题了,所以使用 &#x997F;&#x6C49;&#x5F0F;也未尝不可。

​ JDK源码举例:

​ 该类位于 java.lang包下,首先将构造方法私有化,声明了一个私有的静态变量并且对该变量进行对象实例的创建,再创建一个公有的静态方法返回这个对象实例,这是比较常用的一种实现单例模式的方式。

2.2 懒汉式

// ①懒汉式:线程不安全
static class Singleton {
    // 1.构造器私有化,其他类不能new
    private Singleton() {}
    private static Singleton instance;
    // 2.向外部暴露一个静态的公共方法
    public static Singleton getInstance() {
        // 3.instance == null时进行实例化
        if ( instance == null ) {
            // new Singleton()不是一个原子操作,JVM中会进行大致[创建对象-分配内存-对象初始化]等过程,在这之前instance都为null
            // 多线程情况下,多个线程同时执行到该位置,线程获取到时间片后会继续执行,就可能创建多个实例
            instance = new Singleton();
        }
        return instance;
    }
}
// ②懒汉式:线程安全(方法上添加 synchronized 关键字)
static class Singleton {
    // 1.构造器私有化,其他类不能new
    private Singleton() {}
    private static Singleton instance;
    // 2.向外部暴露一个静态的公共方法, synchronized 保证线程安全
    public static synchronized Singleton getInstance() {
        // 3.instance == null时进行实例化
        if ( instance == null ) {
            instance = new Singleton();
        }
        return instance;
    }
}

​ 懒汉式就是在创建对象实例前先判断是否已经创建,但是由于对象实例的创建并不是一个原子过程,所以会出现线程安全问题,可以在方法上添加 synchronized解决,当然会牺牲一定的性能。基于以上原因,不推荐使用 &#x61D2;&#x6C49;&#x5F0F;的方式实现单例模式。

​ 如何证明对象实例的创建不是一个原子操作? 字节码指令可以从侧面证明。

// Java源码
public class Test {
    public Test getTest() {
        return new Test();
    }
}

红框1的位置有三条字节码指令,这还只是字节码的层面,再往低层还会有更多的步骤,所以很明显 对象实例的创建不是一个原子操作

2.3 双重检查锁

static class Singleton {
    // 1.构造器私有化,其他类不能new
    private Singleton() {}
    // 2.volatile保证多线程下的可见性
    private static volatile Singleton instance;
    // 3.向外部暴露一个静态的公共方法
    public static Singleton getInstance() {
        // 3.非空判断
        if ( instance == null ) {
            // 4.同步代码块
            synchronized (Singleton.class) {
                // 5.再次非空判断(保证多线程下的单例)
                if ( instance == null ) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

&#x53CC;&#x91CD;&#x68C0;&#x67E5;&#x9501;是最复杂的一种单例模式实现方式,我把它拆分成三个问题:

① 为什么 synchronized 不加到方法上?

​ 如果添加到方法上,两次非空判断就没有必要了,一次就够了,就转化成了 &#x61D2;&#x6C49;&#x5F0F;&#xFF08;&#x7EBF;&#x7A0B;&#x5B89;&#x5168;&#xFF09;,这种方式效率不高,因为每次调用都需要获取锁和释放锁。

② 为什么要做两次非空判断?

​ 之前也提到过: 对象实例的创建不是一个原子操作。线程安全问题也是出在这一过程中的,解决方案就是添加 synchronized关键字,但是添加到方法上效率又太低了;

​ 既然问题是出现在对象实例创建的过程中,那么只对这一段代码进行同步操作(加锁对象就是当前的Class对象,因为对象实例只有一个);

​ 第一层的非空判断是为了如果对象实例已经创建完成了,就不需要再次进入同步代码块了,直接返回创建好的对象实例即可。

③ 为什么要加 volatile

根据对象实例创建的字节码指令可以看出对象实例的创建大致分为三步:

​ ① 在堆内存中分配对象内存

​ ② 调用 <init></init>方法,执行对象实例的初始化

​ ③ 将对象引用赋给静态变量

大家应该对 JMM模型happens-before有所了解,简单来说 JMM模型是对编译器和处理器的约束, happens-before是对开发者的约束。

编译器和处理器在实际运行时,为了执行效率可能会对指令进行重排序的操作,虽然单线程中不会影响执行结果,但是如果是多线程就会出现问题。

像对象实例创建过程的三条指令中②③就有可能会被优化为③②,但是①一定会先执行,因为②③依赖于①,此时执行顺序为①③②,其他线程就会获取到一个未初始化的对象,导致执行出错。

volatile关键字的语义包含两个:

​ ① 保证可见性

​ ② 禁止指令重排序(所以添加 volatile后,执行顺序就是①②③了)

​ JDK源码举例:

​ 该类是位于 java.lang包下的 System类,经典的 &#x53CC;&#x91CD;&#x68C0;&#x67E5;&#x9501;实现方式。

2.4 静态内部类

static class Singleton {
    // 1.构造器私有化,其他类不能new
    private Singleton() {}
    // 2.静态内部类,Singleton类加载的时候不会加载内部类,只有用到内部类时才回去加载内部类(保证懒加载)
    private static class SingletonInstance {
        private static final Singleton instance = new Singleton();
    }
    // 3.向外部暴露一个静态的公共方法,此时会装载SingletonInstance,类装载时是线程安全的(保证线程安全)
    public static Singleton getInstance() {
        return SingletonInstance.instance;
    }
}

​ 这是一种很巧妙的方式,相对于 &#x997F;&#x6C49;&#x5F0F;来说,不需要在类的初始化阶段就创建对象实例,只有在需要(即调用 getInstance()方法)的时候才会进行对象实例创建,线程安全也由JVM保证。

​ JDK源码举例:

​ 上图是 java.lang.Short源码中的内部类,将常用的整数保存到缓存池当中;下图是访问缓存池中的整数。类似的还有 java.lang.Integerjava.lang.Long等包装类。

2.5 枚举

// enum实际上是extends抽象类java.lang.Enum
enum Singleton {
    instance
}

字节码反编译看下:

enum关键字修饰的类实际上继承了 java.lang.Enum<e extends enum<e></e>

枚举类中声明的实例实际上是 public static final修饰的常量

上图为枚举类中 <clinit></clinit>方法的字节码指令,也就是类的初始化阶段需要执行的逻辑(即将静态变量,静态代码块整合到一块)。

红框1:创建 Singleton枚举类对象实例,实际上调用了 java.lang.Enum类的构造器(即 <init>&#x65B9;&#x6CD5;</init>),构造器参数是(”INSTANCE”, 0),可以通过 ldc #7iconst_0看出来;对象实例创建完成后,将实例引用赋给 INSTANCE常量。

红框2:将上一步创建的对象实例引用保存到枚举类内部数组 $VALUES中,外部可以通过 values()方法返回所有的枚举对象引用;数组的创建是在 iconst_1anewarray,意思是 创建一个长度为1的引用类型数组

Original: https://www.cnblogs.com/yushixin1024/p/16488495.html
Author: 飒沓流星
Title: 设计模式之单例模式

原创文章受到原创版权保护。转载请注明出处:https://www.johngo689.com/589264/

转载文章受原作者版权保护。转载请注明原作者出处!

(0)

大家都在看

  • SQL执行顺序

    首先select语句中都会用到哪些关键字: select ,from,join,where,group by,having,order by,limit 其次,要知道每执行一步就会…

    Java 2023年6月9日
    095
  • 【Java面试】面试自閟了!工作5年的小伙伴今天面试被吊打问我,并行和并发有什么区别?

    “并行和并发有什么区别?”关于这个问题,很多工作5年以上的同学都回答不出来。或者说,自己有一定的理解,但是不知道怎么表达。大家好,我是Mic,一个工作了14…

    Java 2023年6月16日
    087
  • Jquery选择器个人总结

    1、选择第一级子节点 通过> 或者children方法实现 $(‘#XtraTabPage8>.datagrid-ftable’) $(&#82…

    Java 2023年6月8日
    071
  • 这四种对象属性拷贝方式,你都知道吗?

    当 get/set太繁琐时;当 BeanUtils无法拷贝集合时;当。。。可能,你需要好好看看这篇文章,文末附完整示例代码。 在做业务的时候,为了隔离变化,我们会将 DAO查询出来…

    Java 2023年6月5日
    0106
  • Java之收集很好的Java学习资料地址+博客

    https://blog.insanecoder.top/tcp-packet-splice-and-split-issue/ http://blog.csdn.net/qilix…

    Java 2023年5月29日
    079
  • Mysql中有哪些减少回表的操作?

    回表是指,InnoDB 在普通索引 a 上查到主键 id 的值后,再根据一个个主键 id 的值到主键索引上去查整行数据的过程。 使用覆盖索引 如果 select 的数据列只用从索引…

    Java 2023年6月5日
    075
  • 【转】【OPenGL】理解OpenGL拾取模式(OpenGL Picking)

    在用OpenGL进行图形编程的时候,通常要用鼠标进行交互操作,比如用鼠标点选择画面中的物体,我们称之为拾取(Picking),在网上看了很多OpenGL拾取的文章,但大多是只是介绍…

    Java 2023年5月29日
    0109
  • MySQL之事务隔离级别和MVCC

    事务隔离级别 事务并发可能出现的问题 脏写 事务之间对增删改互相影响 脏读 事务之间读取其他未提交事务的数据 不可重复读 一个事务在多次执行一个select读到的数据前后不相同。因…

    Java 2023年6月16日
    069
  • Java基础中Int类型变量值互换的几种方法

    在很多时候,我们会使用到将两个 整型变量值进行互换,比如冒泡排序,通过判断来将数组变量的值逐步交换,那么怎么交换值才能最有效最节省空间呢? 首先,我们会想到的,用一个零时变量来做中…

    Java 2023年6月5日
    054
  • 如何下载网页上的背景图片?

    如何下载网页上的背景图片?(以谷歌浏览器为例) 具体步骤(以研控为例) 一、登录网址 我们这里以网站 研控 为例,网址放在这里:https://www.yankong.org/ 我…

    Java 2023年6月5日
    0203
  • 鸿蒙(HarmonyOS)开发笔记一:系统简介

    1. HarmonyOS是什么? 根据华为官方的说明,harmonyOS是分布式,提供新交互,新服务,万物互联的一款操作系统。下面简单介绍一下何为:新服务,新交互 基于harmon…

    Java 2023年6月16日
    0111
  • day04-3服务器推送新闻

    多用户即时通讯系统04 4.编码实现03 4.7功能实现-服务器推送消息功能实现 4.7.1思路分析 服务器推送新闻,本质其实就是群发消息 在服务器启动一个独立线程,专门负责推送新…

    Java 2023年6月15日
    0114
  • 阿里云智能编码插件进行了一个上新大动作

    大家好!我们是阿里云云效智能代码天团!旨在用人工智能解放各位开发者的生产力!或许你们关注过我们的话会知道,我们有一个超酷的产品它叫Alibaba Cloud AI Coding A…

    Java 2023年6月8日
    0110
  • java动态编译

    Java动态编译:http://pfmiles.github.io/blog/dynamic-java/ 动态编译代码:https://github.com/giraffe/Dyn…

    Java 2023年5月29日
    087
  • 【设计模式】汉堡中的设计模式——观察者模式

    【设计模式】汉堡中的设计模式——观察者模式 【设计模式】汉堡中的设计模式——观察者模式 情景带入 为什么关注公众号就可以 发布者-订阅者模式与观察者模式 + 观察者模式(Obser…

    Java 2023年6月5日
    099
  • org导出html时图片链接部分会出错(已解决)

    当在org中写入类似下列图片链接的时候,在导出的时候会出错 http://images.cnblogs.com/cnblogs_com/csophys/432523/o_%E7%9…

    Java 2023年6月7日
    072
亲爱的 Coder【最近整理,可免费获取】👉 最新必读书单  | 👏 面试题下载  | 🌎 免费的AI知识星球