单例模式:懒汉和饿汉

宁睿  论坛元老 | 2025-4-17 06:14:37 | 显示全部楼层 | 阅读模式
打印 上一主题 下一主题

主题 1949|帖子 1949|积分 5857

目录
一、关于筹划模式
二、单例模式是什么
2.1 饿汉模式
2.2 懒汉模式
三、单例模式和多线程
3.1 饿汉模式
 3.2 懒汉模式


 

一、关于筹划模式

单例模式是一种筹划模式,说它之前先来聊聊筹划模式是什么。
筹划模式,类似于于棋谱(大佬把一些对局整个推演过程写出来)
筹划模式,就相当于步伐员的棋谱。大佬们把一些范例的问题场景,整理出来,而且针对这些场景,代码该怎么写,具体方案给出一些指导和建议。
框架是属于‘硬性要求’,筹划模式是‘软性要求’,目标是一致的。
二、单例模式是什么

单例模式是筹划模式中经典也是比较简单的模式
   单个实例(对象)
  欺压要求,某个类,在某个步伐中,只有唯逐一个实例(不允许创建多个实例,不允许new多次)
  1. class Test{
  2. }
  3. //对象/实例
  4. Test t = new Test();
复制代码
单例模式,欺压要求一个类不能创建多个对象,通过一些编程技巧,告竣上述的欺压要求。
在代码中,假如创建多个实例,直接编译失败
单例模式两种情况:饿汉模式和懒汉模式。接下来会根据这两种情况举行展开
2.1 饿汉模式

饿,代表着迫切,想要尽早创建实例
  1. class Singleton{
  2.     //静态成员的初始化,是在类加载的阶段出发的
  3.     //类加载往往就是在程序已启动就会触发
  4.     private static Singleton instance = new Singleton();
  5.     //后续通过get方法获取这里的实例
  6.     public static Singleton getInstance(){
  7.         return instance;
  8.     }
  9.     //单例模式中的“点睛之笔”,在外面进行new操作,都会编译失败
  10.     private Singleton() {
  11.     }
  12. }
  13. public class demo1 {
  14.     public static void main(String[] args) {
  15.         Singleton t1 = Singleton.getInstance();
  16.         Singleton t2 = Singleton.getInstance();
  17.         System.out.println(t1 == t2);
  18.         //会报错
  19.         //Singleton t3 = new Singleton();
  20.     }
  21. }
复制代码
2.2 懒汉模式

懒 和 饿 是相对的,懒是只管晚的创建实例(甚至可能不创建了),延迟创建
懒 在计算机里是褒义词,另一个寄义,是高效率
  1. //懒汉模式
  2. class SingletonLazy{
  3.    
  4. private static volatile SingletonLazy instance = null;
  5.     public static SingletonLazy getInstance() {
  6.         if(instance == null){
  7.               instance = new SingletonLazy();
  8.             }
  9.         return instance;
  10.     }
  11.     private SingletonLazy(){
  12.     }
  13. }
  14. public class demo2 {
  15.     public static void main(String[] args) {
  16.         SingletonLazy t1 = SingletonLazy.getInstance();
  17.         SingletonLazy t2 = SingletonLazy.getInstance();
  18.         System.out.println(t1 == t2);
  19.         //SingletonLazy t3 = new SingletonLazy();
  20.     }
  21. }
复制代码
三、单例模式和多线程

上述内容,都是引子,接下来才是正题
上述懒汉/饿汉模式,是否是线程安全?假如不是,该咋办?
这两个版本的getInstance在多线程环境下调用,是否会出bug?
我们可以一个个来看
3.1 饿汉模式

  1. class Singleton{
  2.     private static Singleton instance = new Singleton();
  3.    
  4.     public static Singleton getInstance(){
  5.         return instance;
  6.     }
  7.     private Singleton() {
  8.     }
  9. }
复制代码
这里只涉及了 return,而return是 读利用,线程安全
   String 不可变对象,自然线程安全
   3.2 懒汉模式

  1. class SingletonLazy{
  2.    
  3. private static volatile SingletonLazy instance = null;
  4.     public static SingletonLazy getInstance() {
  5.         if(instance == null){
  6.             instance = new SingletonLazy();
  7.         }
  8.         return instance;
  9.     }
  10.     private SingletonLazy(){
  11.     }
  12. }
复制代码
  if(instance == null){
          instance = new SingletonLazy();
  }
  这部分可能会涉及到 多线程 的修改
   = 利用是原子的, +=  -=  这些是非原子的
  这里可能会出现bug,这就导致懒汉模式这个写法,getInstance方法是线程不安全的。
怎么办理?
加锁是一个通例的利用
但也要注意加锁的位置,我们希望的是:
条件和修改都能打包成原子的利用
  1. private static Object locker = new Object();
  2. public static SingletonLazy getInstance() {
  3.         synchronized(locker){
  4.             if(instance == null){
  5.                 instance = new SingletonLazy();
  6.             }
  7.         }
  8.         return instance;
  9.     }
复制代码
不是写了synchronized,代码就肯定安全,肯定得具体问题具体分析
引入加锁后,后实行的线程就会在加锁位置阻塞,阻塞到前一个线程解锁,当后一个线程进入条件的时候,前一个线程已经修改完毕,Instance不再为null,就不会举行后续new的利用。
也可以举行方法加锁
  1.     public synchronized static SingletonLazy getInstance() {
  2.         if(instance == null){
  3.             instance = new SingletonLazy();
  4.         }
  5.         return instance;
  6.     }
复制代码
但是
加锁引入新的问题:
当把实例创建好之后,后续再调用getInstance,此时都是直接实行return,假如只是举行if判定+return,纯粹的读利用了,读利用不涉及线程安全问题
但是,每次调用上述方法,都会触发一次加锁利用,虽然不涉及线程安全问题,但是多线程情况下,这里的加锁,就会相互阻塞,影响步伐的实行效率
所以我们可以如许:按需加锁
真正涉及到线程安全的时候,再加锁,不涉及的时候,就不加锁
假如实例已经创建过了,就不涉及线程安全问题提。假如还没创建,就涉及线程安全问题
  1. public static SingletonLazy getInstance() {
  2.     if(instance == null){ // 判断是否需要加锁
  3.         synchronized (locker){ //
  4.             if(instance == null){ // 判断是否需要new对象
  5.                 instance = new SingletonLazy();
  6.             }
  7.         }
  8.     }
  9.     return instance;
  10. }
复制代码
单线程中,连续两个相同的if,是毫无意义的,单线程中,实行流就只有一个,上一个if和下一个if是一样的
但是多线程中,两次判定之间,可能存在其他线程,就把if中的Instance变量给修改了,也就是导致了这里的两次if的结论可能不同

再仔细分析,上述代码,仍然存在问题:
t1线程在读取Instance的时候,t2线程举行修改,是否存在内存可见性问题?
可能存在,编译器优化这件事变,非常复杂
为了稳妥起见,可以给Instance直接加上一个volatile,从根本上杜绝,内存可见性问题
  1. private static volatile SingletonLazy instance = null;
复制代码
这里更关键的问题是:指令重排序
指令重排序也是编译器优化的一种体现形式,编译会在逻辑稳定的前提下,调整代码实行的先后顺序,以到达提升性能的效果
   编译器优化,每每不只是javac(Java语言的编译器,Java Compile)通常是javac和jvm配合的效果(甚至是利用系统也要配合)
  1. instance = new SingletonLazy();
复制代码
实例化对象,通常包罗以下三个步骤:

  • 申请内存空间
  • 在空间上构造对象(初始化)
  • 内存空间的首地址,赋值给引用变量
正常来说,这三个步骤,是按照1 2 3 如许的步骤来实行的
但是,在指令重排序下,可能是 1 3 2如许的顺序
单线程下1 2 3还是1 3 2 其实无所谓
假如是1 3 2 如许的顺序实行,多线程下 是会出现bug的
对应的办理方法也要用到 volatile,而且上面也碰巧把这个问题办理了:
  1. private static volatile SingletonLazy instance = null;
复制代码
Volatile 的功能有两方面:

  • 确保每次独缺利用,都是读内存 内存可见性
  • 关于该变量的读取和修改利用,不会触发重排序

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

宁睿

论坛元老
这个人很懒什么都没写!
快速回复 返回顶部 返回列表