单例模式:类在进程中只有唯一的一个实例
解释一下:
可以把猫理解为一个类
它的实例有:橘猫,布偶,美短,虎斑…
这里介绍2种较为常见的单例模式的写法
- 饿汉模式(急迫)
- 懒汉模式(从容)
🔎1.示例(饿汉模式)
饿汉模式:线程天然就是安全的
什么是饿汉模式呢?
举个栗子
有一个人已经一天没吃饭了
他很饿很饿,这时候如果有一桌饭菜摆在他面前,他是很急迫的渴望去吃饭的
🌻示例代码
class Single { private static Single instance = new Single(); public static Single getInstance() { return instance; } private Single() { } } public class Test1 { public static void main(String[] args) { Single s1 = Single.getInstance(); //注意这里会报错,因为构造方法被private修饰 //Single s2 = new Single(); } } • 1 • 2 • 3 • 4 • 5 • 6 • 7 • 8 • 9 • 10 • 11 • 12 • 13 • 14 • 15 • 16 • 17 • 18 • 19 • 20 • 21
上述代码因为构造方法被private修饰,所以只能有一个实例(单例模式)
注意:这段代码通过反射仍然可以获得一个新的实例(这里不讨论这种情况)
那么为什么饿汉模式天然就是安全的呢?
回想以下可能导致线程安全问题的几点原因:
(1)抢占式执行 (2)多个线程修改同一个变量 (3)操作不是原子性的 (4)内存可见性 (5)指令重排序
上述代码只有一个return instance,只是读取instance,虽然会发生抢占式执行,但并没有修改变量
所以饿汉模式天然就是安全的
🔎2.示例(懒汉模式)
懒汉模式:多线程下可能无法保证创建对象的唯一性(不安全)
什么是懒汉模式呢?
举个栗子
你和你的女朋友刚刚在家吃过晚饭
吃饭的碗可以现在不去刷,等到下次再吃饭的时候再去洗碗
这就是懒汉模式,比较从容
🌻示例代码
class SingleLazy { private static SingleLazy instance = null; public static SingleLazy getInstance() { if(instance == null) { instance = new SingleLazy(); } return instance; } private SingleLazy() { } } public class Test2 { public static void main(String[] args) { SingleLazy s1 = SingleLazy.getInstance(); //注意这里会报错,因为构造方法被private修饰 //Single s2 = new Single(); } }
上述代码因为构造方法被private修饰,所以只能有一个实例(单例模式)
注意:这段代码通过反射仍然可以获得一个新的实例(这里不讨论这种情况)
🌻原因分析
这里我们来一点点剖析上面的代码
🌼case1
我们看到这里面 instance默认是null,而不是new一个实例,这和饿汉模式是有区别的
(1)那么为什么这么做呢?
(2)到底是通过饿汉模式书写单例模式更好还是通过懒汉模式书写单例模式更好呢?
(1)这样做是为了避免有可能我并没有调用该类.却把类实例化了,造成了浪费(注:这里只是通过这个这个例子解释区别)
(2)答案是通过懒汉模式,想象以下,有一个10G的文件,饿汉模式是全部加载完才能够打开(这可能需要几分钟),懒汉模式是加载显示器能显示的那一部分,然后再用再去加载,虽然对硬盘的访问次数多了,但是还是懒汉模式更香啊!!!
🌼case2
安全性分析
对于这段代码,如果多个线程同时调用,就可能产生多个对象了
如图
当 t1线程先去判断 instace == null,发现 instance是null
此时 t2线程也正好去判断 instace == null,发现 instance是null
然后 t1和 t2线程分别去执行 new SingleLazy()这个操作,就可能会产生多个对象
解决方法
通过加锁🔒,合并2个步骤为1个步骤,变成原子性
public static SingleLazy getInstance() { //注意这里的是静态方法,()里面需要写成(类.class) synchronized (SingleLazy.class) { if (instance == null) { instance = new SingleLazy(); } } return instance; }
🌼case3
对case2改动代码进行优化
我们知道,加锁🔒就会产生阻塞(运行速度变慢)
但是不加锁🔒,数据的准确性又会产生问题,这里我们能不能进行一点点优化呢?
请看代码
public static SingleLazy getInstance() { //注意这里的是静态方法,()里面需要写成(类.class) if(instance == null) { synchronized (SingleLazy.class) { if (instance == null) { instance = new SingleLazy(); } } } return instance; }
注意看这2段代码的区别,多了个 if(instance == null)
不加 if()进行判断,每次都得去加锁阻塞,无论此时的 instance是否为null
加上if()后,如果 istance不是null直接返回,是再去进行加锁操作
这样可以省去了不少时间
回顾一下
对于刚开始的代码
这里是存在安全问题的,所以我们需要加锁🔒去保证安全
当加锁🔒后,无论此时的 instance是否为null,都需要去执行加锁操作
于是就进行了一点优化
🌼case4
安全性分析
那么针对这段代码,是否就没有问题了呢?
指令重排序可能会产生错误
红色圈圈代表可能会触发指令重排序问题
指令:
(1)创建内存
(2)调用构造方法
(3)将内存地址赋给引用
对于上述指令
其执行顺序可能为(1)(2)(3)
也可能是(1)(3)(2)
当执行顺序为(1)(3)(2)时
先创建了内存,然后将内存地址赋给引用,如果此时其他线程去调用 getInstance方法,就会发现 instance不是 null,但是由于并没有进行步骤(2)调用构造方法,也就不是一个实例,所以就会引发错误
解决方法
加volatile禁止指令重排序
完整代码
class SingleLazy { volatile private static SingleLazy instance = null; public static SingleLazy getInstance() { //注意这里的是静态方法,()里面需要写成(类.class) if(instance == null) { synchronized (SingleLazy.class) { if (instance == null) { instance = new SingleLazy(); } } } return instance; } private SingleLazy() { } } public class Test2 { public static void main(String[] args) { SingleLazy s1 = SingleLazy.getInstance(); //注意这里会报错,因为构造方法被private修饰 //Single s2 = new Single(); } }
🔎3.总结
饿汉模式:天然就是安全的
懒汉模式:不安全
- 解决方法:
- 加锁,将 if 和 new 操作结合成一起
- 双层if,进行优化
- 使用volatile禁止指令重排序
🔎结尾
创作不易,如果对您有帮助,希望您能点个免费的赞👍
大家有什么不太理解的,可以私信或者评论区留言,一起加油