当前位置: 首页 > article >正文

【多线程】单例模式

文章目录

  • 1. 单例模式
    • 1.1 什么是单例模式
    • 1.2 为什么使用单例模式
    • 1.3 实现单例模式
      • 1.3.1 饿汉模式
      • 1.3.1 懒汉模式

1. 单例模式

1.1 什么是单例模式

单例模式是一种创建型设计模式,它确保一个类只有一个实例,并提供一个全局访问点来访问该实例。

单例 =单个实例(对象)

1.2 为什么使用单例模式

使用单例模式,就可以对咱们的代码进行一个更严格的校验和检查。
示例:

有的时候代码中,需要使用一个对象,来管理/持有大量的数据,此时有一个对象就可以了。比如,一个对象管理了10G的数据,如果你不小心创建出多个对象,内存空间就会成倍增长,机器就顶不住了。

期望:

让机器 (编译器)能够对代码中的指定的类,创建的实例个数进行校验,如果发现创建多个实例了,就直接编译报错这种。如果能做到这一点,就可以非常放心的编写代码,不必担心因为失误创建出多个实例了。

1.3 实现单例模式

1.3.1 饿汉模式

饿汉模式是单例模式的一种实现方式,其核心特点是在类加载时就创建单例对象,确保在程序运行期间该类的单例对象始终存在。

饿汉模式在多线程中天然就是线程安全

class Singleton {
    private static Singleton instance = new Singleton();
    public static Singleton getInstance() {
        return instance;
    }
    private Singleton() {}
}
public class Danli1 {
    public static void main(String[] args) {
        // Singleton s = new Singleton();
        Singleton s1 = Singleton.getInstance();
        Singleton s2 = Singleton.getInstance();
        System.out.println(s1 == s2);
    }
}

这个引用就是我们希望创建出唯一实例的引用。static 静态的,指的是类属性, instance就是Singleton类对象里面持有的属性。每个类的类对象只有一个,类对象的static属性自然也就只有这一个。

private static Singleton instance = new Singleton();

这段代码的作用就是想要使用这个类的实例,就需要通过这个方法来获取实例。不应该在其他的代码中重新new这个对象,而是直接调用这个方法来获取到现成的对象

public static Singleton getInstance() {
        return instance;
    }

单例模式的点睛之笔,为了防止其他类new一个对象,这里直接将构造方法私有化,其他类调用不了构造方法自然也就创建不了这个实例。

1.3.1 懒汉模式

懒汉模式是单例模式的一种实现方式,其核心特点是在第一次需要使用单例对象时才进行实例化,而不是像饿汉模式那样在类加载时就创建实例,这样可以节省资源。

package Thread;

/**
 * 懒汉模式
 * */
class SingletonLazy {
    private static Object locker = new Object();
    private volatile static SingletonLazy instance = null;//注意这个volatile关键字的使用
    public static SingletonLazy getInstance() {
        //如果instance为null,就说明是首次调用,首次调用需要考虑到线程安全的问题,就要加锁
        //如果是非null,就说明是后续的操作,就不必加锁
        if (instance == null) {
            synchronized (locker){
                if (instance == null) {
                    instance = new SingletonLazy();
                }
            }
        }
        return instance;
    }
    private SingletonLazy() {}
}
public class Danli2 {
    public static void main(String[] args) {
        SingletonLazy s1 = SingletonLazy.getInstance();
        SingletonLazy s2 = SingletonLazy.getInstance();
        System.out.println(s1 == s2);
    }
}

关键问题:在多线程中,并发调用getInstance方法,这两个代码是否线程安全?

对于饿汉来说,getlnstance 直接返回 Instance 实例。这个操作本质上就是"读操作",线程安全。
对于懒汉来说,getlnstance 先判断再创建 Instance 实例。这个操作有读也有写,线程不安全(上述代码通过修改改为线程安全)。

修改方案:
1)加锁
在多线程中,有可能线程1执行到判断,切换到了线程2执行完了所有创建实例,这时切换回线程1时,线程1在已经判断的基础上又创建了新的实例,bug出现!!!

为了让代码执行正确,将 if 和 new 两个操作,打包成一个原子

synchronized (locker){
                if (instance == null) {
                    instance = new SingletonLazy();
                }
            }

2)双重校验锁
在多线程中,如果instance已经被创建,应该快速的返回该实例。但是判断操作还是在锁里,每次调用都先加锁再解锁,此时效率就非常低了!!!(因为加锁就意味着可能会产生阻塞)

为了提高效率,在锁的外层加一个判断,判定是否需要加锁。如果是第一次调用该方法,也就是创建实例对象,则进行加锁解锁;如果是第二次之后调用该方法,则仅需返回第一次创建好的实例对象就行,提高效率。

    public static SingletonLazy getInstance() {
        //如果instance为null,就说明是首次调用,首次调用需要考虑到线程安全的问题,就要加锁
        //如果是非null,就说明是后续的操作,就不必加锁
        if (instance == null) {
            synchronized (locker){
                if (instance == null) {
                    instance = new SingletonLazy();
                }
            }
        }
        return instance;
    }

3)指令重排序
指令重排序就JVM编译器优化的一种方式,调整原有代码的执行顺序,保证逻辑不变的情况下,提高程序的效率,但是在提升效率的同时会引起线程安全问题。

instance = new SingletonLazy();

对于上面这一个行代码,可以拆分为三大步骤:

(1)申请一段内存空间
(2)在这个内存上调用构造方法,创建出这个实例
(3)把这个内存地址赋值给 instance 引用变量

正常情况下,上述代码是按照1.2.3的顺序执行的,但是编译器也会优化成1.3.2的顺序执行,此时,指令重排序就会造成问题。
如果线程1先执行1.3,调度走,此时instance不是null,但是指向的其实是一个尚未初始化的对象。但是未初始化的对象会被线程2判定为 instance != null ,就会直接 return。如果线程2继续使用 instance 里面的属性和方法,就会出现问题(此时这里的属性都是未初始化的“全0”值)。就可能会引起代码的逻辑出现问题。

为了解决这问题,还是引入 volatile
volatile 有两个功能
1.保证内存可见性,每次访问变量必须都要重新读取内存,而不会优化到寄存器/缓存中
2.禁止指令重排序。针对这个被 volatile 修饰的变量的读写操作相关指令,是不能被重排序的!!!

private volatile static SingletonLazy instance = null;//注意这个volatile关键字的使用

http://www.kler.cn/a/590495.html

相关文章:

  • Unity学习之Shader总结(一)
  • Docker入门篇2:查看容器、运行容器、启动和停止容器、删除容器
  • Android PC 要来了?Android 16 Beta3 出现 Enable desktop experience features 选项
  • 【STM32】NVIC(嵌套向量中断控制器)
  • Android之RecyclerView列表拖动排序
  • Vue3项目白屏问题深度解析:从AI辅助诊断到性能优化实战
  • 《灵珠觉醒:从零到算法金仙的C++修炼》卷三·天劫试炼(49)万鸦壶焚网络 - 网络延迟时间(Bellman-Ford)
  • Spring boot+mybatis的批量删除
  • 【AI】深度学习与人工智能应用案例详解
  • LIMS系统在纸制品制造的应用 内检实验室LIMS系统提升纸制品质控
  • Postman发送GET请求示例及注意事项
  • Vue.js 事件处理与修饰符详解
  • 2. qt写带有槽的登录界面(c++)
  • 玩转python:通俗易懂掌握高级数据结构-collections模块之UserDict
  • 人工智能之数学基础:从线性变换理解矩阵范数和矩阵行列式
  • 第一中标人!晶科能源入围大唐集团19.5GW光伏组件集采
  • 遥感新态势:Sentinel - 2多光谱指数与AI深度融合
  • 卡内基梅隆大学研究人员推出 PAPRIKA:一种微调方法,使语言模型能够发展出不局限于特定环境的通用决策能力
  • 基于javaweb的SpringBoot博客商城管理系统设计与实现(源码+文档+部署讲解)
  • 通过 Python 爬虫提高股票选股胜率