10.Java单例模式全解析:饿汉式与懒汉式实现及线程安全深度剖析

目录

一、上节课复习回顾

二、本节课重点引入

设计模式背景

设计模式的意义

三、单例模式概念详解

四、饿汉模式实现

五、饿汉模式原理剖析

六、懒汉模式初探

七、两种设计模式对比

八、懒汉模式线程安全问题探讨

九、懒汉模式加锁尝试

十、双重检查锁定实现

加锁的目的分析:

​编辑

十一、指令重排序问题与volatile修饰

十二、总结


一、上节课复习回顾

在正式进入本篇博客之前,我们先来回顾一下上节课关于多线程的核心知识点:

  1. 线程的概念、进程和线程的区别

  2. Thread类的使用

  3. 线程的状态

  4. 线程安全

  5. 锁 => 死锁

  6. volatile

  7. wait / notify、synchronized

二、本节课重点引入

本节课我们将深入探讨设计模式(单独的学科)。

设计模式背景

  • 程序员水平参差不齐,大佬(少)、菜鸡(多)。

  • 编译器优化只能解决执行效率的问题。

  • 开发效率:程序员在写这个代码的时候,快慢。

  • 目前来看,开发效率比执行效率更重要。

  • 执行效率是可以通过升级硬件来弥补的(硬件性能突飞猛进,成本逐年降低)。

  • 程序员的人力成本,逐年上升。

  • 主要的成本,在人力成本上。

设计模式的意义

  • 软件开发中,存在很多这样的重复的问题场景。

  • 总结提炼出固定的套路/解决方案------定式。

  • 设计模式类似于棋谱------按照棋谱来下,棋力一定不会弱。

  • 大佬们总结出了很多的设计模式,不同的编程语言中也会存在不同的设计模式。

  • 设计模式不是只有23种!!!

三、单例模式概念详解

一种设计模式,可以保证你一个Java进程中某个类只有唯一的一个实例。

为啥不能创建多个呢?

假设我们的服务器是要加载100GB的数据到内存中------广告系统。

一组服务器包含了若干个分片,每个分片中又有若干个服务器。

代码中搞了一个类DataCenter,类的构造方法中,进行上述的数据加载(读硬盘文件,把内容放到内存中)。

这个对象一旦创建好,就是消耗100GB的内存。

这个类,不能随便创建多个实例的。

不小心创建两个,都可能会导致程序出现严重问题。

某个类,本身从需求功能上来说,一个实例就足够了------完全可以使用单例模式了。

既然是要"单例",只new一次就好,不去new多次,有必要搞单例模式吗?

靠人来保证是不靠谱。

机器/程序/编译器则比较靠谱,

此处的单例模式,强制让某个类只能存在一个实例。

当你尝试创建多个实例,直接编译报错!

四、饿汉模式实现

只能确保当前进程内,没法保证多个进程之间~~

java 复制代码
package thread;

// 此处要求 Singleton 类只能有一个实例。
class Singleton {
    // 加了 static,当前的成员成为"类属性",在类对象上面的,类对象只有一个实例。
    // 此处 instance 就可以保证在当前 java 进程中只有一份。
    private static Singleton instance = new Singleton();

    public static Singleton getInstance() {
        return instance;
    }

    // 单例模式最关键的要点,禁止构造方法被外部使用。
    private Singleton() {
    }
}

public class Demo24 {
    public static void main(String[] args) {
        // 此时其他代码想 new 这个实例,直接编译报错。
        // Singleton s = new Singleton();
        Singleton s1 = Singleton.getInstance();
        Singleton s2 = Singleton.getInstance();
        System.out.println(s1 == s2);
    }
}

五、饿汉模式原理剖析

每个 java => class => 被 JVM 加载到内存,得到类对象。

"饿" => 急迫------近似认为是程序一启动。

六、懒汉模式初探

java 复制代码
package thread;

// 懒汉的单例模式
class SingletonLazy {
    private static SingletonLazy instance = null;

    // 懒汉模式的关键在于,把实例的创建时机推迟了,推迟到第一次使用的时候,创建。
    public static SingletonLazy getInstance() {
        if (instance == null) {
            instance = new SingletonLazy();
        }
        return instance;
    }

    private SingletonLazy() {
    }
}

七、两种设计模式对比

通常认为懒汉方式更好~~

八、懒汉模式线程安全问题探讨

懒是"褒义词"------懒,想偷懒,才思考,才去找新的方法,创造新的工具。

灵活 => 贬义词 => 容易出错 => 搞不好把年终奖给毙了~~

呆板 => 褒义词 => 不容易出错

设计模式,就是限制你的灵活~~

那么提一个问题:这俩版本是否是线程安全的??

多线程------多个线程并发的调用 getInstance,是否会有问题~~

先明白,return 是一个"读操作"。

++所以饿汉模式,天然就是线程安全的。++

getInstance中只是进行读操作。

多个线程对同一个变量进行读取,没有问题的~~

++那么,懒汉模式呢?先说结论,他是线程不安全的!++

因为条件判定和赋值,放到一起,才算是完整的逻辑~~才是原子性。

但是懒汉模式虽然线程不安全,不安全只是出现在实例化之前的时候。

一旦实例创建好了,后续再调用 getInstance,都不会有问题了(都是读操作了)。

九、懒汉模式加锁尝试

此处本质上是 if 和 = 这两个操作被打包成原子。

十、双重检查锁定实现

进一步分析------

此处的这个代码,只有在实例化之前存在线程安全问题。

一旦实例化完毕之后,线程安全问题就消失了~~

此处即使把 instance 实例化之后,此处的代码的仍然会加锁 ~~

正常如果不加锁的话,代码就不会产生阻塞~~

虽然加锁内部的执行逻辑看似很块,实际上它背后线程调度的成本是非常高的~~

对于单线程来说,连续写两次的条件,结论一定是相同的!!不需要重复写~~

对于多线程来说,就不适用了~~

走到那 => 阻塞 => 很长时间。

锁阻塞的时间里,很可能存在其他线程,把 instance 给修改了。

导致外层 if 和 里层 if 结论不相同了!!

完整合适的代码:

java 复制代码
package thread;

// 懒汉的单例模式
class SingletonLazy {
    private volatile static SingletonLazy instance = null;

    // 作为加锁锁的对象,由于是要在 static 方法中使用该对象,对象本身也得是 static 的。
    private static Object locker = new Object();

    // 懒汉模式的关键在于,把实例的创建时机推迟了,推迟到第一次使用的时候,创建。
    public static SingletonLazy getInstance() {
        //第一个if语句用来判断是否还需要加锁
        if (instance == null) {
            synchronized (locker) {
                // 第二个if语句判定是否需要创建实例
                if (instance == null) {
                    instance = new SingletonLazy();
                }
            }
        }
        return instance;
    }

    private SingletonLazy() {
    }
}

🌟这也是一个非常经典的秋招面试题:

你需要回答清楚三个点:

  1. 为什么要有两个if,他们分别的作用各是什么

  2. 为什么要加锁

  3. 为什么instance这个引用变量要加volatile关键字?(主要解决指令重排序问题)

加锁的目的分析:

十一、指令重排序问题与volatile修饰

指令重排序引起的问题~~也是编译器的一种优化手段~~

本质是调整你指令执行的顺序~~从而让代码的效率更高~~

此处应该禁止指令重排序,把这样的编译器优化关闭掉~~

volatile------

++内存可见性 / 指令重排序优化都是出现在针对某个变量的 读写 操作中~~++

当使用 volatile 修饰 instance 的时候,此时围绕 instance 变量读写相关的优化,就都被关闭了~~

确保从内存进行读写,确保操作顺序不会被打乱~~

十二、总结

上述逻辑在多线程下可能就出问题了!!!

相关的例子,只有单例模式这一个孤证~~

针对指令重排序对线程安全的影响,只是根据网上的常见说法汇总的情况,真实性不可考,

没法写代码验证~~ 不可控因素太多了~~

我个人角度是存疑的,

很可能在现在 jdk 的版本中,即便不加 volatile 也没事了~~

不过宁可信其有,不可信其无。

写代码的时候多个 volatile 倒是没啥成本~~

面试官也是这么理解的~~

相关推荐
龙亘川8 小时前
一网统管AI平台民生业务实践:基于城市数字底座赋能公积金业务服务升级
大数据·安全·智慧城市·开源软件·数据可视化·政务
kybs19918 小时前
全球灾害数据分析可视化 毕业设计-附源码66794
vue.js·spring boot·mysql·安全·django·c#·asp.net
LorryJovens10 小时前
【LAAP科研】双系统具身AGI范式研究——基于LAAP认知架构与Jev概率决策模型的系统性技术调研与范式验证
人工智能·gpt·安全·架构
其实防守也摸鱼10 小时前
内网穿透与反向代理:原理、工具与实战指南
android·大数据·运维·安全·网络安全·自动化·渗透
山东科恩光电10 小时前
提升安全性的关键技术:安全触边在工业自动化中的应用解析
安全
LONGZETECH11 小时前
一线职教实测:风光 580 汽车故障诊断仿真系统,破解实车实训四大核心痛点
人工智能·学习·安全·架构·汽车
Mikko711 小时前
jackson-databind 升到 2.21.6 就安全了吗?jackson-core 是另一个坐标,它那条 high 全局库至今没收
java·后端·安全·json
赛博守夜人11 小时前
反舞弊系列之十四:商业地产与零售招商中的“转租差价与免租期寻租”
安全
xiaoqiMikko13 小时前
你升的是 jackson-databind,中招的是 jackson-core:一个超长 JSON key 就能吃掉 200MB 堆
java·安全