欢迎光临
我们一直在努力

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

目录

一、上节课复习回顾

二、本节课重点引入

设计模式背景

设计模式的意义

三、单例模式概念详解

四、饿汉模式实现

五、饿汉模式原理剖析

六、懒汉模式初探

七、两种设计模式对比

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

九、懒汉模式加锁尝试

十、双重检查锁定实现

加锁的目的分析:

​编辑

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

十二、总结


一、上节课复习回顾

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

  • 线程的概念、进程和线程的区别

  • Thread类的使用

  • 线程的状态

  • 线程安全

  • 锁 => 死锁

  • volatile

  • wait / notify、synchronized

  • 二、本节课重点引入

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

  • 设计模式背景

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

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

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

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

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

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

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

  • 设计模式的意义

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

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

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

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

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

  • 三、单例模式概念详解

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

    为啥不能创建多个呢?

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

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

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

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

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

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

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

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

    靠人来保证是不靠谱。

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

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

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

    四、饿汉模式实现

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

    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 加载到内存,得到类对象。

    “饿” => 急迫——近似认为是程序一启动。

    六、懒汉模式初探

    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 结论不相同了!!

    完整合适的代码:

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

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

    赞(0)
    未经允许不得转载:171主机测评 » 10.Java单例模式全解析:饿汉式与懒汉式实现及线程安全深度剖析
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址