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

单例模式:懒汉和饿汉

目录

一、关于设计模式

二、单例模式是什么

2.1 饿汉模式

2.2 懒汉模式

三、单例模式和多线程

3.1 饿汉模式

 3.2 懒汉模式


 

一、关于设计模式

单例模式是一种设计模式,说它之前先来聊聊设计模式是什么。

设计模式,类似于于棋谱(大佬把一些对局整个推演过程写出来)

设计模式,就相当于程序员的棋谱。大佬们把一些典型的问题场景,整理出来,并且针对这些场景,代码该怎么写,具体方案给出一些指导和建议。

框架是属于‘硬性要求’,设计模式是‘软性要求’,目标是一致的。

二、单例模式是什么

单例模式是设计模式中经典也是比较简单的模式

单个实例(对象)

强制要求,某个类,在某个程序中,只有唯一一个实例(不允许创建多个实例,不允许new多次)

class Test{}//对象/实例
Test t = new Test();

单例模式,强制要求一个类不能创建多个对象,通过一些编程技巧,达成上述的强制要求。

在代码中,如果创建多个实例,直接编译失败

单例模式两种情况:饿汉模式和懒汉模式。接下来会根据这两种情况进行展开

2.1 饿汉模式

饿,代表着迫切,想要尽早创建实例

class Singleton{//静态成员的初始化,是在类加载的阶段出发的//类加载往往就是在程序已启动就会触发private static Singleton instance = new Singleton();//后续通过get方法获取这里的实例public static Singleton getInstance(){return instance;}//单例模式中的“点睛之笔”,在外面进行new操作,都会编译失败private Singleton() {}
}public class demo1 {public static void main(String[] args) {Singleton t1 = Singleton.getInstance();Singleton t2 = Singleton.getInstance();System.out.println(t1 == t2);//会报错//Singleton t3 = new Singleton();}
}

2.2 懒汉模式

懒 和 饿 是相对的,懒是尽量晚的创建实例(甚至可能不创建了),延迟创建

懒 在计算机里是褒义词,另一个含义,是高效率

//懒汉模式
class SingletonLazy{private static volatile SingletonLazy instance = null;public static SingletonLazy getInstance() {if(instance == null){instance = new SingletonLazy();}return instance;}private SingletonLazy(){}
}public class demo2 {public static void main(String[] args) {SingletonLazy t1 = SingletonLazy.getInstance();SingletonLazy t2 = SingletonLazy.getInstance();System.out.println(t1 == t2);//SingletonLazy t3 = new SingletonLazy();}
}

三、单例模式和多线程

上述内容,都是引子,接下来才是正题

上述懒汉/饿汉模式,是否是线程安全?如果不是,该咋办?

这两个版本的getInstance在多线程环境下调用,是否会出bug?

我们可以一个个来看

3.1 饿汉模式

class Singleton{private static Singleton instance = new Singleton();public static Singleton getInstance(){return instance;}private Singleton() {}
}

这里只涉及了 return,而return是 读操作,线程安全

String 不可变对象,天然线程安全

 3.2 懒汉模式

class SingletonLazy{private static volatile SingletonLazy instance = null;public static SingletonLazy getInstance() {if(instance == null){instance = new SingletonLazy();}return instance;}private SingletonLazy(){}
}

if(instance == null){

        instance = new SingletonLazy();

}

这部分可能会涉及到 多线程 的修改

= 操作是原子的, +=  -=  这些是非原子的

这里可能会出现bug,这就导致懒汉模式这个写法,getInstance方法是线程不安全的。

怎么解决?

加锁是一个常规的操作

但也要注意加锁的位置,我们希望的是:

条件和修改都能打包成原子的操作

private static Object locker = new Object(); public static SingletonLazy getInstance() {synchronized(locker){if(instance == null){instance = new SingletonLazy();}}return instance;}

不是写了synchronized,代码就一定安全,一定得具体问题具体分析

引入加锁后,后执行的线程就会在加锁位置阻塞,阻塞到前一个线程解锁,当后一个线程进入条件的时候,前一个线程已经修改完毕,Instance不再为null,就不会进行后续new的操作。

也可以进行方法加锁

    public synchronized static SingletonLazy getInstance() {if(instance == null){instance = new SingletonLazy();}return instance;}

但是

加锁引入新的问题:

当把实例创建好之后,后续再调用getInstance,此时都是直接执行return,如果只是进行if判定+return,纯粹的读操作了,读操作不涉及线程安全问题

但是,每次调用上述方法,都会触发一次加锁操作,虽然不涉及线程安全问题,但是多线程情况下,这里的加锁,就会相互阻塞,影响程序的执行效率

所以我们可以这样:按需加锁

真正涉及到线程安全的时候,再加锁,不涉及的时候,就不加锁

如果实例已经创建过了,就不涉及线程安全问题提。如果还没创建,就涉及线程安全问题

public static SingletonLazy getInstance() {if(instance == null){ // 判断是否需要加锁synchronized (locker){ // if(instance == null){ // 判断是否需要new对象instance = new SingletonLazy();}}}return instance;
}

单线程中,连续两个相同的if,是毫无意义的,单线程中,执行流就只有一个,上一个if和下一个if是一样的

但是多线程中,两次判定之间,可能存在其他线程,就把if中的Instance变量给修改了,也就是导致了这里的两次if的结论可能不同

再仔细分析,上述代码,仍然存在问题:

t1线程在读取Instance的时候,t2线程进行修改,是否存在内存可见性问题?

可能存在,编译器优化这件事情,非常复杂

为了稳妥起见,可以给Instance直接加上一个volatile,从根本上杜绝,内存可见性问题


private static volatile SingletonLazy instance = null;

这里更关键的问题是:指令重排序

指令重排序也是编译器优化的一种体现形式,编译会在逻辑不变的前提下,调整代码执行的先后顺序,以达到提升性能的效果

编译器优化,往往不只是javac(Java语言的编译器,Java Compile)通常是javac和jvm配合的效果(甚至是操作系统也要配合)

instance = new SingletonLazy();

实例化对象,通常包括以下三个步骤:

  1. 申请内存空间

  2. 在空间上构造对象(初始化)

  3. 内存空间的首地址,赋值给引用变量

正常来说,这三个步骤,是按照1 2 3 这样的步骤来执行的

但是,在指令重排序下,可能是 1 3 2这样的顺序

单线程下1 2 3还是1 3 2 其实无所谓

如果是1 3 2 这样的顺序执行,多线程下 是会出现bug的

对应的解决方法也要用到 volatile,并且上面也碰巧把这个问题解决了:

 private static volatile SingletonLazy instance = null;

Volatile 的功能有两方面:

  1. 确保每次独缺操作,都是读内存 内存可见性

  2. 关于该变量的读取和修改操作,不会触发重排序

相关文章:

  • 深入探索函数的奥秘:从基础到进阶的编程指南
  • uniapp(Vue)开发微信小程序 之 保存图片到本地
  • 其利天下即将亮相第21届(顺德)家电电源与智能控制技术研讨会
  • 确保连接器后壳高性能互连的完整性
  • Go-zero:JWT鉴权方式
  • 车载刷写架构 --- 刷写流程中重复擦除同一地址的问题分析
  • 【MySQL】索引事务
  • 把城市变成智能生命体,智慧城市的神奇进化
  • Android开发案例——简单计算器
  • 【经验记录贴】活用shell,提高工作效率
  • 【Python进阶】列表:全面解析与实战指南
  • 设计模式每日硬核训练 Day 13:桥接模式(Bridge Pattern)完整讲解与实战应用
  • ThreadPoolExecutor 多线程用requests请求一个地址的时候为什么会报错,而多进程用requests请求一个地址的时候不会报错,为什么?
  • 04.Python代码NumPy-通过索引或切片来访问和修改
  • 【正点原子STM32MP257连载】第四章 ATK-DLMP257B功能测试——4G模块ME3630测试
  • TinyEngine 2.4版本正式发布:文档全面开源,实现主题自定义,体验焕新升级!
  • Java转Go记录:Slice解密
  • 负载均衡的实现方式有哪些?
  • 【大模型】DeepSeek + Coze 打造个人专属AI智能体使用详解
  • uniapp-商城-27-vuex 通用方法
  • 免费做调查问卷的网站/免费开源网站
  • wordpress做外贸网站的劣势/培训行业seo整站优化
  • 企业网站规划原则/抖音广告投放代理商
  • 织梦57网站的友情链接怎么做/百度竞价推广的优势
  • 九江广安建设网站/谷歌seo最好的公司
  • 网站建设我们的优势/友链交易交易平台