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 倒是没啥成本~~

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

相关推荐
紫金桥软件1 小时前
国产化信创浪潮下,如何选择组态软件
安全·国产化·scada·国产工业软件·监控组态软件
txg6661 小时前
MirrorFuzz:利用共享漏洞与大模型的深度学习框架 API 模糊测试
人工智能·深度学习·安全·网络安全
网安情报局2 小时前
高防IP是什么?原理是什么?
安全
2601_959480152 小时前
Moneta Markets亿汇:“网络安全认证提升信任”
安全·web安全
Survivor0012 小时前
Codex Harness 安全沙箱机制原理
安全·ai·安全架构
薛定猫AI3 小时前
【深度解析】Claude Fable 5 全面评测:安全防护机制、基准测试与实战性能深度拆解
网络·安全
ylscode3 小时前
Claude Fable 5遭多智能体越狱攻击:Anthropic最强AI安全防线被击穿,12万字符系统提示泄露
网络·人工智能·安全
sou_time3 小时前
从 0 到 商用:AI Agent x SKILL x MCP 全栈实战教程:L3 商用篇:性能 / 成本 / 可观测性 / 安全 / 部署
人工智能·安全
BEOL贝尔科技3 小时前
“温度异常威胁样本安全?”安装温湿度监控设备实时监测+快速响应是关键!
人工智能·安全·数据分析