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 天前
第 2 篇:设计第一套字节码:Opcode、Instruction 与 Constant Pool
前端·javascript·安全
泯泷1 天前
第 1 篇:从 1 + 2 开始:亲手写出第一台 JSVM
前端·javascript·安全
Flynt5 天前
npm v12 来了:allowScripts 默认关闭,我的项目差点跑不起来
安全·npm·node.js
冬奇Lab10 天前
Skill 系列(02):Skill 安全风险——三类攻击面的实战测试
人工智能·安全·开源
Aphasia31113 天前
VPN 与内网穿透
安全
Mr_愚人派14 天前
当"Claude"不再是 Claude:一次第三方 API 代理引发的 AI 身份伪造排查实录
人工智能·安全
DaLi Yao15 天前
【无标题】
人工智能·安全
Alsn8615 天前
等待学习-学习目录:Docker 容器安全攻防
学习·安全·docker
网络研究院15 天前
2026年网络安全
网络·安全·法律·法规·趋势·发展
treesforest15 天前
AI安全系统如何识别异常访问?IP风险识别正在成为关键能力
网络·人工智能·tcp/ip·安全·web安全