[版权申明] 非商业目的注明出处可自由转载
出自:shusheng007
设计模式汇总篇,一定要收藏:永不磨灭的设计模式
概述
单例模式是GOF 23种设计模式中最简单的一种,但是使用却一点不少,上帝果然喜欢简洁。单例模式虽然简单,但是也还是有很多可以探讨的地方。
咱们就来聊聊Java中单例模式的5种写法吧,以及各种设计模式的优劣,最后讨论一下你喜欢哪一种,为什么?
类型
创建型(creational)
难度
1颗星
定义
某个类只有一个实例,且自行实例化并向整个系统提供此实例
你是不是觉的终于有一个可以看得懂的设计模式的定义啦?如果是这样,说明你原本对它就比较熟悉。
使用场景
当你希望整个系统运行期间某个类只有一个实例时候
UML
这里有一张图,看见了吗?图在你心中
实例
楚中天(外号林蛋大)在外包公司干了快两年了,这期间都是外派王二狗公司的。两年中蛋大总有种寄人篱下的感觉,这不王二狗看蛋大表现很好,就向公司推荐将其转为正式。公司会通过面试来对新员工定级,下面是他们之间的对话:
面试官:林蛋大,是叫林蛋大吧?
楚中天:内心活动:蛋大nmb,瞎吗?小学毕业了吗?老子叫楚中天!脱口而出:老师,其实我叫楚中天,您可以叫我中天。
面试官:哦,好的,蛋大。你能谈谈单例模式吗,你能用几种方法实现单例模式,他们之间都有什么利弊吗?
楚中天:balabala...
单例模式两个核心尿点:
- 如何保证单例
多线程环境下如何保证系统中只有一个实例?类实现序列化时如何保证?如何保证不能通过反射创建新的实例?
- 如何创建单例
这块又分为懒汉模式与饿汉模式。
其实也很好理解,懒汉的意思就是这个类很懒,只要别人不找它要实例,它都懒得创建。饿汉模式正好相反,这个类很着急,非常饥渴的要得到自己的实例,所以一有机会他就创建了自己的实例,不管别人要不要。
单例模式的5种写法:
1: 静态常量
这个简单粗暴,在类加载时候就创建了实例,属于饿汉模式。其是线程安全的,这一点由JVM来保证,但是有一个缺点,可以通过反射创建新的实例。如果让你改进,你怎么弄呢?评论区留下你的见解
这里原来有一笔误,感谢评论区小伙伴的指正
public class Singleton1 {
private final static Singleton1 INSTANCE = new Singleton1();
private Singleton1() {
}
public static Singleton1 getInstance(){
return INSTANCE;
}
}
2: 单null检查
使用这个写法的程序员应该说水平不是太高,这种写法应该被抛弃。其不是线程安全的,也就是说在多线程环境下,系统中有可能存在多个实例。除此之外,和上面一样通过反射也可以创建新的实例。
public class Singleton2 {
private static Singleton2 instance;
private Singleton2() {
}
public static Singleton2 getInstance() {
if (instance == null) {
instance = new Singleton2();
}
return instance;
}
}
针对线程不安全问题,让我们尝试改进一下它:
我们将实例化过程放到同步块里可以解决问题吗?如下所示
public static Singleton2 getInstance() {
if (instance == null) {
synchronized (Singleton2.class) {
instance = new Singleton2();
}
}
return instance;
}
很可惜,这种方式也是不行的,让我们简单分析一下为什么不行。
假设我们有两个线程 T1与T2并发访问getInstance
方法。当T1执行完if (instance == null)
且instance为null时,其CUP执行时间被T2抢占,所以T1还没有创建实例。T2也执行if (instance == null)
,此时instance肯定还为null,T2执行创建实例的代码,当T1再次获得CPU执行时间后,其从synchronized
处恢复,又会创建一个实例。
那我们之间将同步基本升级到获取实例的方法基本可以吗?恭喜你,可以!但是,又是该死的但是!但是程序的性能被极大的降低了。下面的Singleton3
给获取实例的方法添加了synchronized
。这样的话,线程是安全了,但是却极大的降低了性能,因为大部分情况下线程都只是去获取这个实例,但现在却要排队。
public class Singleton3 {
private static Singleton3 instance;
private Singleton3() {
}
public static synchronized Singleton3 getInstance() {
if (instance == null) {
instance = new Singleton3();
}
return instance;
}
}
那有没有既不降低性能又保证线程安全的方法呢?Java5后答案是肯定的,因为现在Java5已经绝迹江湖了,所以可以说答案是肯定的。
3: 双重null检查
为了解决上面单null检查的线程安全与程序性能的问题,出现了double-check的方式。此方式的关键一个点就在于volatile
关键字,其阻止了虚拟机指令重排,使得我们的双检查得以实现。在Java5之前,这种双重检查的方式即使加上了volatile
也没有用,还是不能用,因为JVM有bug。
所以double-check方式一定要加volatile
关键字,否则由于指令重拍会导致单例失败。关于volatitle
可以参考秒懂Java并发之volatile关键字引发的思考
public class Singleton4 {
private static volatile Singleton4 instance;
private Singleton4() {
}
public static Singleton4 getInstance() {
if (instance == null) {
synchronized (Singleton4.class) {
if(instance ==null){
instance = new Singleton4();
}
}
}
return instance;
}
}
我最开始接触到这种double-check的写法时觉得很奇诡,为什么要搞那么多check?后来我明白了:
第一重check为了提高访问性能。因为一旦实例被创建,所有的check永远为假。其实你把第一重check去掉也没问题,只是访问性能降低了,那样就变成和直接同步方法一样了。
第二重check是为了线程安全,确保多线程环境下只生成一个实例。具体分析可以参考单check部分。第一重ckeck可以被多个线程进入,但是第二重check却只能排队进入
4: 静态内部类
这种方式其实很棒,既是线程安全的,也是懒汉式的,那个实例只有在你首次访问时候才会生成。我们完全可以使用这种方式替换double-check方式。
public class Singleton5 {
private Singleton5() {
}
private static class SingletonInstance {
private final static Singleton5 INSTANCE = new Singleton5();
}
public static Singleton5 getInstance() {
return SingletonInstance.INSTANCE;
}
}
5: 枚举
最牛逼的其实是这哥们儿,以上所有方式均存在一个问题,即通过反射的方式可以创建多个实例。如果你的类实现了序列化,那还要防止序列化生成多个实例的问题。而枚举保证了线程安全,保证了反射安全,保证了序列化...
但是,但是,但是实际项目中却很少有人用enum来实现单例...
public enum Singleton6 {
INSTANCE;
}
总结
5种实现单例模式的方式已经聊完了,除了使用枚举,你有办法防止反射破坏单例这个问题吗?小伙伴们踊跃发言,留言区咱们讨论一下
源码
GitHub源码地址:design-patterns
文章评论
强强强
第三点,那里双null,然后链接,链接过去的 CSDN,图是错的,没有save。 load和store是一对,read和write是一对,剩下的4个 是 赋值和使用,加锁和解锁
第一次见第4种方法,学习了,作者牛逼!