- 单例模式:是一种设计模式 
  - 设计模式,类似于“棋谱”,就是固定套路,针对一些特定的场景,给出一些比较好的解决方法
- 只要按照设计模式来写代码,就可以保证代码不会太差,保证代码的下限
 
设计模式
设计模式
- 设计模式并非只有 23 种
- 23 这个数字是有三个大佬(GOF),写了一本设计模式的书,这本书中谈到了 23 种设计模式
- 设计模式也适合变成语言相关的,有些设计模式,是在给语法填坑
- 有的语言本身语法设计的更好,不太依赖设计模式
- 这 23 种设计模式是针对 C# 这个语言来谈的,比较适用于 C++,Java、C#
- 校招中,掌握 4~5 个就好了
- 设计模式适合具有一定的编程经验之后再学习,因为缺少经验,难以理解人家为什么这样写
- 编程这个圈子中,“灵活”是贬义词(易出 bug),“死板”才是褒义词(稳健)
- 设计模式就是针对编写代码过程中的“软性约束”(不是强制的,选择性遵守,但遵守当然有好处)
框架
- 针对一些特定的场景问题,大佬们把基本的代码已经写好了,大部分逻辑也都写好了,留出来一些空位,让你在空位上填充一些自定制的逻辑
- 框架就是针对编写代码过程中的“硬性约束”
- Java 圈子中,特别讲究框架
单例模式
- 开发者,希望有的类在一个进程中,不应该存在多个实例(对象)
- 此时,就可以使用单例模式(单个实例/单个对象),只有唯一实例 
  - 比如在JDBC中- JDBC => DataSourse数据源描述数据库在哪
- 一般来说,一个程序中,只有一个数据库,对应的mysql服务器只有一份,此时DataSourse这个类没有必要创建出多个类
- 此时就可以使用单例模式,描述DataSourse,避免不小心创建出了多个实例
 
- 比如广告系统 
    - 广告数据都是要加载在内存中的,这里的查询就是直接查内存 hash 表,避免直接插数据库(查数据库太慢)
- 所以就在代码中专门搞了个类,管理这些数据,此时这个类也就是案例模式
- 这个类,一个实例就是几百G 内存空间
 
 
- 比如在
前提是“一个进程中”,如果有多个进程,自然每个进程中都可以有一个实例
- Java 中,单例模式的实现有很多种写法,这里我们介绍两种最主流的写法:饿汉模式和懒汉模式
理解
在你们家,每次吃完饭后,如果是你的妈妈洗碗,那一般都是吃完后立即就把碗洗了
 但如果是你洗碗,就会把碗先泡一会再洗,但这样容易忘记,一拖,就拖到了下顿使用碗的时候才会洗
你妈妈这种洗碗习惯就相当于是“饿汉模式”
 你这种洗碗模式就相当于是“懒汉模式”
饿汉模式
“饿”的意思是“迫切”,在类被加载的时候,就会创建出这个单例的实例
- 在 Singleton类中,成员变量instance要用static修饰,这样instance就变成了“类成员”
 类成员初始化,就是在Singleton这个类被加载的时候(近似于程序启动的时候)
class Singleton {  
    //static修饰,instance 成员变量就是“类成员”  
    private static Singleton instance = new Singleton();  
}
- 程序启动,类就加载了,这个类一加载,这里的初始化就进行了,于是这里的实例就创建了
- 这就叫实例创建的非常迫切,就叫“饿汉模式”
- 不过有了实例还不够,还需要外面能对实例进行使用,我们再创建一个方法
class Singleton {  
    //static修饰,instance 成员变量就是“类成员”  
    private static Singleton instance = new Singleton();  
	
	//获取实例对象
	public static Singleton getInstance() {  
        return instance;  
    }
}
- 之后我们需要使用实例的时候,只需要调用这个 getInstance方法就好了
- 现在只要我们不在其他代码中,new这个类,每次需要使用都通过getInstance方法来调用到这个实例,此时这个类就是单例的了
- 但这个设定其实不科学,凭什么你说别人都不去 new 这个类,这就相当于是一个君子协议,口头约定一下。但万一不遵守呢?
- 所以“ 只要我们不在其他代码中,new这个类 ”这个才是单例模式中主要需要解决的问题,防止别人不小心 new 了对象
类的使用者的想法都很简单,“用就对了”,但类的设计者需要考虑的事就多了
- 如何避免这个的实例不小心被 new了一下
- 单例模式最关键一步:将类的构造方法设为私有
class Singleton {  
    //static修饰,instance 成员变量就是“类成员”  
    private static Singleton instance = new Singleton();  
	
	//获取实例对象
    public static Singleton getInstance() {  
        return instance;  
    }  
    
    //最关键的一步
    private Singleton() {}  
}
- 这就意味着在类的外面,就无法调用构造方法,也就无法创建实例了 
单例模式只能避免别人的“失误”,但无法应对别人的“故意攻击”:
- 你不是 private 吗,我通过反射拿到构造方法,通过反射 API 来调用,不就能 new 出实例了吗
- 除了反射,还可以通过“序列化反序列化”打破上述单例模式
懒汉模式
计算机中,“懒”是褒义词。谈到“懒”,效率会更高,是高效的代名词
 在这里,推迟了创建实例的时机,实例会在第一次使用的时候才会创建
经常有这种情况:
 中午的时候,需要使用 4 个碗
 晚上的时候,只需要使用 2 个碗,此时只需要洗两个碗就可以了
- 洗两个碗,比洗四个碗更高效
- 能不搞就不搞,很多时候就可以把这部分开销就省下了
 “这里的优劣只是在计算机中,不是在生活中!!!”
比如,有一个编辑器,打开一个非常大的文本文档,有两种方式:
- 一启动,就把所有的文本内容都读取到内存中,然后再显示到界面上[饿汉]
- 启动之后,只加载一小部分数据(一个屏幕能显示的最大数据),随着用户进行翻页操作,再按需地加载剩下的内容[懒汉]
 毋庸置疑,我们更青睐于“懒汉模式”
class SingletonLazy {  
	//先把实例的引用设为 null,不着急创建实例
    private static SingletonLazy instance = null;  
  
    public static SingletonLazy getInstance() {  
        if(instance == null) {  
            instance = new SingletonLazy();  
        }        
        return instance;  
    }
    
    private SingletonLazy() {}
}
- 先把实例的引用设为 null,不着急创建实例
- 当首次调用 getInstance,由于此时引用为null,就会进入if分支,从而创建实例
- 后续再重复调用 getInstance,结果都不会创建实例,直接返回
- 还是要将构造函数设为 private,避免被不小心new
是否线程安全
上述写的“饿汉“和“懒汉“单例模式代码,是否是线程安全的?(如果是多线程环境下,调用 getInstance,是否会有问题呢?)
- 饿汉模式
public static Singleton getInstance() {  
        return instance;  
    } 
无线程安全问题
- 这里的 return instance只是一个“读操作”,没有涉及到“多个线程对变量进行修改”
- 懒汉模式
public static SingletonLazy getInstance() {  
        if(instance == null) {  
            instance = new SingletonLazy();  
        }        
        return instance;  
    }
有线程安全问题
- 这里的赋值操作就涉及到“修改”了,还有一个“判定”操作结合在一起,这样就非常容易产生“线程安全”问题了
对于 判定+赋值 操作,产生的线程安全问题:
if(instance == null) {  
    instance = new SingletonLazy();      
} 
| 调度顺序 | 线程 | t1 | t2 | 
|---|---|---|---|
| if (istance == null) | |||
| if (instance == null)由于 instance仍然为null,所以t2会认为这个条件也是满足的 | |||
| instance = new SingletonLazy();创建实例 | |||
| instance = new SingletonLazy();由于 t2刚刚完成了条件判断,所以 t2也会执行创建实例的逻辑 | 
- 上述逻辑中,就创建了两个实例
- 第二次创建,覆盖了 instance的值,使得第一次创建的实例没有引用指向,很快就会被垃圾回收机制给消除掉
- 但即使如此,仍然认为上述代码是存在 bug的- 因为实际上,构造方法内部可能会执行很多的逻辑
 
“先判定,再修改”,这种代码模式,是属于典型的线程不安全代码,因为判定和修改之间可能涉及到线程的切换
如何解决线程安全问题
1. 线程切换导致的线程安全
通过加锁,来解决问题
- 出现线程安全问题,是因为 if判定和new操作之间出现了线程切换,出现了逻辑上的穿插
- 所以需要锁,把 if和new打包成一个整体就可以了
public static SingletonLazy getInstance() {  
    synchronized (locker) {  
        if (instance == null) {  
            instance = new SingletonLazy();  
        }    
    }    
    return instance;  
}
- 这样,if判定和new操作就是一个“原子”了
- return加不加到锁里面无所谓- 此处的矛盾是 if和new中间出现线程切换,引起逻辑错误,而后面的return不会受到影响
- 无论 return是在本线程还是其他线程,此处的return的值都是instance内存中的最新值
 
- 此处的矛盾是 
2. 加锁导致的性能下降
加锁之后,确实解决了线程安全问题,到那时加锁却可能会带来阻塞
 如果上述代码,已经 new 完了对象,if 分支再也进不去了,后续的代码,都是单纯的“读操作”,此时 getInstance 不加锁也是线程安全的
 这样就没有必要加锁了
而当前的代码写法,虽然没有线程安全问题了(instance new 出来之后,就都是读操作了),但只要调用了 getInstance,就都会触发加锁操作,此时就会因为加锁,而产生阻塞,啥时候能恢复执行,中间可能是“沧海桑田”,影响性能
因此,针对这个问题,还需要进行进一步改进
- 通过条件判断,在需要加锁的时候才加锁,不需要的时候就不加
public static SingletonLazy getInstance() {  
    //在这个条件中判断当前是否应该加锁  
    if(instance == null) {  
        synchronized (locker) {  
            if (instance == null) {  
                instance = new SingletonLazy();  
            }        
        }    
    }    
    return instance;  
}
- 首次调用,才会有线程安全问题(instance == null),一旦instance已经创建好了,不为null,意味着此时不需要加锁了,没有线程安全问题了
- 如果不在锁前面加上 if,性能会下降很多
3. 指令重排序导致的线程安全
指令重排序,也是一种编译器的优化方式,在不改变原有代码逻辑的条件下,有的时候调整逻辑执行顺序也能提高性能
- 如果是单线程代码,编译器能准确地进行判断
- 如果是多线程代码,编译器可能会出现误判
instance = new SingletonLazy();
这个语句,可以理解是分成了三个步骤:(粗略)
- 给 SingletonLazy对象分配内存空间(买房)
- 在内存空间中,针对这个对象进行初始化(执行构造方法)(装修)
- 将内存空间的地址,赋值给引用变量(收房拿到钥匙)
 编译器可能按照 1、2、3 的顺序执行,也可能按照 1、3、2 的顺序来执行,分配内存的 1 肯定是在最前面,其他的步骤交换
 对于单线程来说,先执行 2 还是 3 本质上是一样的
 在多线程环境下,按照 132 的顺序来执行,是可能出现问题的
| 调度顺序 | 线程 | t1 | t2 | 
|---|---|---|---|
| if(instance == null) {synchronized (locker) {if (instance == null) { | |||
| 1. 分配内存 | |||
| 3. 把地址赋值给引用 (这个赋值使 instance不再为null) | |||
| return instance;因为在 t1线程中,引用已经被赋值了,不再为空,所以直接返回。instance指向了一个没有被初始化,上面的值全为0的内存 | |||
| 2. 执行构造方法,初始化内存 | 
- 在
t2中,由于instance对象内存未初始化,一旦调用的方法里使用了任何instance的成员,都可能是错误的值,可能会引起一系列不可预期的情况
要解决因为指令重排序导致的线程安全,只需要请出 volatile 关键字就可以了
private static volatile SingletonLazy instance = null;
- 编译器就发现了,instance是易失的(易改变的),之后围绕这个变量的优化,就会非常的克制
- 不仅仅会在读取变量的优化上克制,也会在修改变量的优化上克制
- 加上 volatile之后,也能禁止对instance赋值的操作插入到其他操作之间,上述 123 的操作不会再变成 132
volatile 有两个功能:
- 保证内存的可见性
- 禁止指令重排序(针对赋值)




![[Qt][信号与槽][上]详细讲解](https://i-blog.csdnimg.cn/direct/2b844dd74e8342a79a04ea8d0581e017.png)














