• 单例模式【JavaEE初阶】


    一、单例模式的概念

            单例模式是一种常见的设计模式 。单例模式希望:有些对象,在一个程序中应该只有唯一一个实例,就可以使用单例模式 。换句话说,在单例模式下,对象的实例化被限制了,只能创建一个,多了也报错创建不了 

    单例模式的设计:类似于棋谱,是 前辈们已经总结好了的一些固定套路,照着棋谱来下棋,棋就不会下的太差,这就提高了下限 

    二、单例模式的简单实现

    在 Java 里面的单例模式,有很多种实现方式,在这里主要介绍两个大类:饿汉模式、懒汉模式 

    饿汉模式 和 懒汉模式 两种模式,描述了创建实例的时间。

    比如说,想要了解某个资料(大文件 10G,存放在硬盘中),那么 此时使用某个编辑器,打开文件,就会出现两种情况:

    • 饿汉:把 10G 都读到内存中,读取完毕之后 再允许用户进行查看和修改~
    • 懒汉:只读取一点点(当前屏幕能显示出的范围),随着用户翻页,继续再读后续内容~

            所以说,如果是 饿汉模式,那么显示所需要的时间就会比较多;如果是 懒汉模式,那么显示的时间就会比较低,效率就会比较高(也有可能 用户打开文件以后,只看了两眼就关了,后面的大部分都没有读,那么内存读了那么多也没有意义) 

    所以,通常我们都认为,懒汉模式 要比 饿汉模式 更高效 

    像 刷抖音、看小说、看微信、上网浏览内容等 都是借鉴了 懒汉模式 

    2.1 饿汉模式

    饿汉模式 的意思是,程序一旦启动,就会立刻创建实例 。

    这就好比,一个饿了的人,看到一张饼,就会迫不及待的往嘴里塞,我们把它叫做 "饿汉" 

    static关键字 的来龙去脉:

    static 名字叫做 "静态",但是实际上和字面意思没有任何的关系,这是一个历史遗留的问题~

    实际表示的含义是 "类属性 / 类方法",同样的,我们把 不是静态的普通的成员叫做 "实例属性 / 实例方法" 

    Java 里面叫做 "静态" 是因为 C++ 里面表示 "类属性",就是用 static,Java 是从 C++ 那里抄来的;而 C++ 则是因为 引入面向对象之后。需要搞一个方式来定义类属性,就需要引入一个关键字,但是引入新的关键字 成本极高,所以关键字设计者的大佬们 目光就盯住了 旧的关键字 

    于是,static 就中招了,static 原来表示的是 变量放到静态内存区,但是随着时间的推移,系统的进化,已经没有 "静态内存区" 这个说法了,但是 static关键字 还在,于是 "旧瓶装新酒",就用来表示 "类属性 / 类方法" 了~

    此时,"类属性 / 类方法" 和 静不静态 字面意思上没有啥关系,只是 随便找一个之前旧的关键字,现在没啥用了,赋予一个新的功能,仅此而已~

    引入新的关键字成本极高的原因:

    写代码的时候,变量名不能和关键字一样,当引入新的关键字的时候,不可以确定 其他人是不是已经引入了 新的关键字 作为变量名 

    更大的可能是 新的关键字一引入,就会导致已有的一些代码 编译就会失败 

    然后这把火就会烧到了 关键字设计者 的身上~而类属性就长在类对象上,类对象在整个程序中只有唯一一个实例(JVM保证的) ,所以说 类的静态成员就只有唯一一个了 

    1. package thread;
    2. //单例模式:饿汉模式
    3. class Singleton {
    4. //在此处,先把实例给创建出来了
    5. private static Singleton instance = new Singleton();
    6. //后续如果需要这个实例,就需要统一基于 getInstance 方法来获取
    7. public static Singleton getInstance() {
    8. return instance;
    9. }
    10. //为了避免Singleton类不小心被实例化,要把构造方法设为私有
    11. //此时在类外面,其他类想来 new 就不可以了 (通过 编译器的规则来确保只有一个实例对象)
    12. //
    13. private Singleton() {
    14. }
    15. }
    16. public class Demo19 {
    17. public static void main(String[] args) {
    18. //饿汉模式 的调用
    19. Singleton instance = Singleton.getInstance();
    20. Singleton instance2 = Singleton.getInstance();
    21. Singleton instance3 = new Singleton();
    22. System.out.println(instance == instance2); //true
    23. }
    24. }

    运行结果:

    如果不小心 想创建另一个实例,那么就会编译报错了:

    总结:

    static的作用:

    1. static让当前instance属性成为类属性,类属性是长在类对象上的,类对象又是唯一实例的(只在类加载阶段被创建出一个实例),那么就保证这个实例的唯一性
    2. 保证这个实例确实在一定时机中被创建出来

    构造方法设为private,那么除自己类,外面的类都无法new

    使用 静态成员表示实例(唯一性) + 让构造方法设为私有(堵住了 new 创建新实例的口子) 

    按照上面的代码,当 Singleton类 被加载的时候,就会执行到 实例化操作,此时 实例化的时机非常早(非常迫切的感觉),我们把它称为 饿汉模式 

    对于饿汉模式来说,在多线程的情况下,多次调用的是 getInstance() 方法, 而 这个方法只是一个 读操作,对于多线程读操作来说,是线程安全的 

    2.2 懒汉模式

    懒汉模式 的意思是,程序启动,先不着急创建实例,等到真正用的时候,再创建实例 

    这个也很形象,比较 "懒",不想干活,等到必要的时候再去干活 

    1. //懒汉模式的实现
    2. class SingletLazy {
    3. //此处没有立即创建实例
    4. private static SingletLazy instance = null;
    5. //当首次调用 getInstance() 的时候,才会创建实例
    6. public static SingletLazy getInstance() {
    7. if (instance == null) {
    8. instance = new SingletLazy();
    9. }
    10. return instance;
    11. }
    12. //同理,创建构造方法 SingletLazy(),防止该类实例化其他的对象
    13. private SingletLazy() { }
    14. }

    在多线程的情况下,懒汉模式,多次调用 getInstance() 方法,而且涉及到了 两次读操作(读出 instance 是否为空,读出 返回的instance值)和 一次写操作(修改 instance 变量的值),这是线程不安全的 

    当然,一旦实例创建好了以后,后续 if 条件语句就进不去了,此时也就是 全是读操作了,也就线程安全了 

    既然已经明确了,懒汉模式 是线程不安全的,那么 如何解决懒汉模式线程不安全的问题呢?

    办法就是 需要加锁!!!

    通过 加锁 来保证 "判断" 和 "修改" 这组操作是原子的(这里的线程安全本质是读和写这三个操作不是原子的,导致t2读到的数据可能是t1还没来得及写的(脏读))

    1. //懒汉模式的实现
    2. class SingletLazy {
    3. //此处没有立即创建实例
    4. private static SingletLazy instance = null;
    5. //当首次调用 getInstance() 的时候,才会创建实例
    6. public static SingletLazy getInstance() {
    7. synchronized (SingletLazy.class) {
    8. if (instance == null) {
    9. instance = new SingletLazy();
    10. }
    11. }
    12. return instance;
    13. }
    14. //同理,创建构造方法 SingletLazy(),防止该类实例化其他的对象
    15. private SingletLazy() { }
    16. }

    懒汉模式,只是在初始情况下才会有线程不安全的问题,一旦实例创建好了以后,此时就安全了.

    所以说,在后续调用 getInstance 的时候就不应该尝试加锁了 

    如果使用上述的代码,无论 instance 是否为空(是否初始化),都会进行加锁,使得锁竞争加剧,消耗一些没有必要消耗的资源,就会很影响效率了 

    在加锁之前,还需要进行判断 instance 是否为空(是否初始化),如果为空才会进行加锁: 

    1. //懒汉模式的实现
    2. class SingletLazy {
    3. //此处没有立即创建实例
    4. private static SingletLazy instance = null;
    5. //当首次调用 getInstance() 的时候,才会创建实例
    6. public static SingletLazy getInstance() {
    7. if (instance == null) {
    8. synchronized (SingletLazy.class) {
    9. if (instance == null) {
    10. instance = new SingletLazy();
    11. }
    12. }
    13. }
    14. return instance;
    15. }
    16. //同理,创建构造方法 SingletLazy(),防止该类实例化其他的对象
    17. private SingletLazy() { }
    18. }

    分析:

    外层 if 判定当前是否已经初始化好,如果未初始化好,就尝试加锁;如果已经初始化好,那么就接着往下走~

    里层 if 是在多个线程尝试初始化,产生了锁竞争,这些参与竞争的线程 拿到锁之后,再进一步确认,是否真的要初始化~

    当然,上面的代码操作还是有一些问题的 —— 有的线程在读,有的线程在写~

    这就联想起了 —— 内存可见性问题~

    其实,这里的情况 和 之前的情况还不一样,每一个线程都有自己的上下文,都有自己的寄存器内容,按理来说 是不应该会出现优化的~

    但是,实际上也不好说,也并不能保证 编译器优化 是啥样的过程~

    因此,给 instance 加上 volatile 是更加稳健的做法~

    如果不加 volatile 不一定会有问题,但是 稳妥起见,还是加上更好 

    这里的volatile有两个功能:

    1. 解决内存可见性
    2. 禁止指令重排序
    1. //懒汉模式的实现
    2. class SingletLazy {
    3. //此处没有立即创建实例
    4. volatile private static SingletLazy instance = null;
    5. //当首次调用 getInstance() 的时候,才会创建实例
    6. public static SingletLazy getInstance() {
    7. if (instance == null) {
    8. synchronized (SingletLazy.class) {
    9. if (instance == null) {
    10. instance = new SingletLazy();
    11. }
    12. }
    13. }
    14. return instance;
    15. }
    16. //同理,创建构造方法 SingletLazy(),防止该类实例化其他的对象
    17. private SingletLazy() { }
    18. }

    总结:

    懒汉模式 线程的三个要点:

    1. 加锁
    2. 双重 if
    3. volatile(不加可能是错的,但加了一定是正确的)

    关于 单例模式 的内容就先介绍到这里了,当然,在 Java 中也有许多其他的方式 也可以实现单例模式,如 基于枚举、基于内部类 等等 

  • 相关阅读:
    C语言:结构体——关于内存字节对齐图文详解
    基于Python+djangoWeb的校园信息化统计平台
    Mysql—六大日志
    【java数据结构】就是你爱的那颗二叉树
    SQL数据分析之窗口排序函数rank、dense_rank、raw_number与lag、lead窗口偏移函数【用法整理】
    plt.plot
    机器语言编写helloworld
    linqjs记录
    金典成为饿了么小蓝盒首个低碳“盒”伙人:战略合作迎绿色亚运
    面试:并发
  • 原文地址:https://blog.csdn.net/m0_65601072/article/details/128146230