女朋友惊掉下巴问我:单例模式竟有七种写法?
共 6756字,需浏览 14分钟
·
2021-09-06 14:44
接下来,我们要进入的是设计模式篇,关于设计模式,作为程序员的你,肯定在工作中或者面试中遇到过很多次了吧
记得当时18年上大三的时候出去找实习,也问过了解哪些设计模式,不过我个人回答的最多的最详细的大概也就是单例模式了,因为我觉得这个应该是最最好理解的了,虽然有很多种写法,这是为了解决不同环境下的不同问题,当时我应该是把懒汉、饿汉直接都手撕了一遍,也简单的把懒汉和饿汉的区别说了说
当时令我吃惊的是面试官告诉我,单例模式其实有七种写法,甚至可以更多,我当时惊得下巴都掉了,当时我就感觉到了这个行业满满的挑战和满满的知识等着我学习
果不其然,现在越学越觉得自己废物,越学越感觉自己有太多不会的了,不过这个路肯定还是要走下去的,拨开云雾见天明,坚持下去吧
接下来我们来简单介绍下单例模式
单例模式,顾名思义,就是唯一的实例。在当前进程中,有且只有一个单例模式创建的类对象
比如生活中的太阳、只能有一个吧,所以只能有一个实例,这个例子要是用在当年后羿射箭之前不合适,但是现在应该还算是合适的吧
再比如写一个校园管理系统,有一个校长的角色,只能有一个,这个对象在该系统中做成单例就比较合适(其余的是副校长的 亲
这个模式应该是大家最常见的,也是大家认为最简单的了吧,但是实际上这个模式里面还是有很多细节的,也有很多的点值得大家思考的,待会咱们一起看各种写法的时候大家记得带着你的思考和你的问题去学习
单例模式特点
单例模式有如下的特点:
1、一个JVM中有且只有一个实例的存在,构造器私有,外部无法创建该实例
2、提供一个公开的get方法获得唯一的这个实例
有哪些优点呢:
1、省去了new的操作,降低系统内存的使用频率,减轻GC的压力
2、系统中的一些类需要全局单例,比如spring中的controller,再比如人类的太阳
3、避免了资源的重复的占用,减少了内存的开销
其实也是有一些缺点的:
没有接口,不可继承与单一职责原则冲突,一个类应该只关心内部逻辑,而不关心外面怎么样来实例化
先把要介绍的七种给大家说一下,大家有个印象
饿汉式、懒汉式线程不安全和安全版、DCL双重检测锁模式的线程不安全和安全版、静态内部类、枚举类
大家先听个耳熟,下面一一介绍
饿汉式
饿汉式,就是比较饿,于是乎吃的比较早,也就是创建的比较早,会随着JVM的启动而初始化该单例
也正是由于这种类装载的时候就完成了单例的实例化了,不存在所谓的线程安全问题,是线程安全的,相应的缺点就是未达到lazy loading的效果,如果创建的这个单例类始终未用到,便回造成资源浪费
其实在实际开发中,即使知道一定用得到,我们一般也不太会使用这种机制,因为如果单例对象很多,会影响启动的速度,采用懒加载机制是比较节约资源的
开发中很多思想也是采用懒加载,只有当真正用到一个东西的时候才允许它占用相应的资源
/**
* 饿汉式:通过classloader机制避免了多线程的同步问题,在类装载的时候完成实例化
* 优点:写法简单,类装载的时候完成实例化,避免了线程同步的问题
* 缺点:未达到lazy loading的效果,如果始终未用到则可能造成资源浪费
* 适用场景:
*/
public class HungrySingleton {
//1、构造器私有化
private HungrySingleton(){}
//2、类的内部创建对象的实例
private final static HungrySingleton dayu = new HungrySingleton();
//3、将类的内部实例提供一个静态方法返回出去
private static HungrySingleton getInstance(){
return dayu;
}
}
懒汉式(线程不安全、线程安全)
懒汉式咯,就是比较懒,在启动的时候,不会进行该单例对象的创建,只有当真正用到的时候才会去加载这些东西
之所以加懒汉式,大概就是采用了懒加载思想
我们看下面这个懒汉式的代码
/**
* 懒汉式
* 缺点:线程不安全,工作中一般不用
*/
public class NotSafeLazySingleton {
//构造器私有化
private NotSafeLazySingleton(){}
//暂时不加载实例
private static NotSafeLazySingleton dayu;
/**
* 存在线程安全问题
* 线程A到括号dayu == null判断完之后,进入括号内部,
* 此时线程B获得执行权,判断==null也是true,所以也进入
* 此时两个线程便出现了两个dayu对象
* @return
*/
public static NotSafeLazySingleton getInstance(){
if(dayu == null){
dayu = new NotSafeLazySingleton();
}
return dayu;
}
}
其实有过多线程的经验的小伙伴应该很快就看出来了,上面这种懒汉式是有线程安全问题的,当线程A执行到if(dayu == null)这一行的时候,判断为空,true进入括号内部,此时线程A的时间片用完了,到了线程B的执行了,于是乎也会判断为空,进入括号内部
线程B创建了一个NotSafeLazySingleton对象,轮到线程A执行的时候,由于在之前已经判断完进入了括号内部,于是线程A也会创建一个NotSafeLazySingleton对象
GG,这样不是我们想要的效果,这就不属于单例模式了,所以这种在多线程情况下是存在安全问题的
有了问题,自然就是解决咯,可能有的小伙伴也想到了,存在线程安全问题,那就加上线程安全关键字synchronized来解决,于是乎便有了下面的代码,我们给函数加上关键字synchronized,但是这样会造成效率极其低下
所有调用这个方法去使用单例对象的地方都需要排队阻塞知道该锁的释放,在多线程情况下会迅速降低效率
/**
* 懒汉式安全写法
* 缺点:Synchronized关键字导致方法效率低 效率极低
* 优点:线程安全
* 适用场景:实际开发 不推荐使用
*/
public class SafeLazySingleton {
//构造器私有化
private SafeLazySingleton(){}
//暂时不加载实例
private static SafeLazySingleton dayu;
/**
* synchronized导致所有通过该方法获取该对象的时候都要排队
*/
public static synchronized SafeLazySingleton getInstance(){
if(dayu == null){
dayu = new SafeLazySingleton();
}
return dayu;
}
}
所有调用这个方法去使用单例对象的地方都需要排队阻塞知道该锁的释放,在多线程情况下会迅速降低效率,于是有了下面的这种改进方法
只锁其中的部分代码,看下下面的代码
/**
* 本意上是对SafeLazySingelton的改进 因为前面的对整个方法进行加锁的效率实在是太低了
* 但是这种还是不能起到线程同步的作用 和NotSafeLazySingelton类似 只要线程进入了== null的里面
* 此时另一个线程获得CPU分配的时间片 则会出现多个对象
*/
public class NotSafeLaySingleton2 {
//构造器私有化
private NotSafeLaySingleton2(){}
//暂时不加载实例
private static NotSafeLaySingleton2 dayu;
/**
* @return
*/
public static NotSafeLaySingleton2 getInstance(){
if(dayu == null){
synchronized (NotSafeLaySingleton2.class){
dayu = new NotSafeLaySingleton2();
}
}
return dayu;
}
}
上面的这种代码看着有问题吗?
不知道你认真读了上面代码之后,内心是怎么想的,聪明的小伙伴已经发现了事情不是这么简单,发现其中了问题
是的,上面的这种改进方法,貌似实现了效率跟高些,但是会随之带来多线程的问题
线程A判断dayu == null进入括号,还没拿到NotSafeLaySingleton2的锁,时间片消耗完了,此时线程B也判断,发现dayu == null也成立,此时也会进入括号,假设线程B拿到了锁,创建了一个NotSafeLaySingleton2对象,执行完之后释放锁。线程A拿到该锁,会重新创建一个对象,于是出现多例现象
先是通过synchronized加在方法层面解决并发问题,但是随之而来带来效率问题,于是为了提高效率,加在内部,但是加在内部就有了相应的线程安全问题
说了这么多,就是要引出我们下面的线程安全的DCL的单例模式
看下怎么写
双重检查锁模式DCL- double chechked locking(线程安全)
上面那个其实属于单重检查锁模式,我起的名字,因为只检查了一个地方的锁,正是如此也带来了多线程的问题,于是乎就有了下面这种双重检测形势的单例模式了,一起看看吧,稳得一批
/**
* 双重检测单例:稳得一批
* 优点:线程安全 延迟加载 效率相对来说也不错
*使用场景:实际开发中 用的比较多
*/
public class DoubleCheckSingleton {
private static volatile DoubleCheckSingleton dayu;
private DoubleCheckSingleton(){}
/**
* 解决线程安全的问题同时 也解决懒加载问题
* @return
*/
public static DoubleCheckSingleton getInstance(){
if(dayu == null){
synchronized (DoubleCheckSingleton.class){
if(dayu == null){
dayu = new DoubleCheckSingleton();
}
}
}
return dayu;
}
}
上面这种在进入了data == null的内部也会再次判断一次是否还等于空,这种就很好的解决了多线程的问题
这种DCL的单例模式在工作中算是常用的一种了,有效的解决高并发下的单例模式问题
静态内部类
静态内部类加载单例,类加载机制保证线程安全,而且还有一个优点,懒加载,只有在调用getInstance的时候才会加载内部类,才会创建这个对象
外部类被装载的时候,内部类不会立即被装载,调用getInstance才会装载,并且只会装载一次,且不存在线程安全问题
/**
* 静态内部类加载单例
* 优点:类装载机制保证线程安全 懒加载 只有调用getInstance才会加载内部类
* 适用场景:
*/
public class StaticInnerClassSingleton {
private StaticInnerClassSingleton(){}
/**
* 1、外部类被装载时 内部不会立即被装载
* 2、调用getInstance方法时会装载 只会装载一次 且不存在线程安全
*/
private static class SingletonInstance{
private static final StaticInnerClassSingleton dayu = new StaticInnerClassSingleton();
}
//返回静态内部类中的对象
public static StaticInnerClassSingleton getInstance(){
return SingletonInstance.dayu;
}
}
枚举类
枚举类也是可以用作单例模式,而且还很简单
Effective Java作者Josh Bloch所提倡的单例实现的方式就是这种,这种无线程安全问题,还可以防止反序列化重新创建新的对象
/**
* 枚举实现单例
* 优点:简洁 无线程安全问题 还可以防止反序列化重新创建新的对象
* Effective Java作者Josh Bloch提倡的方法
*/
public class EnumSingleton {
public static void main(String[] args) {
//instance和instance2是同一个对象
Singleton instance = Singleton.INSTANCE;
Singleton instance2 = Singleton.INSTANCE;
}
enum Singleton{
INSTANCE;
}
}
总结
设计模式应该属于面试高频,而单例模式又是设计模式的最简单,或者说是最常见的设计模式之一,看完这篇文章,大家应该都知道单例模式的多种写法了,也知道各种的优劣势和相应的使用场景了
我们思考一个问题,为什么要使用单例模式而使用静态方法
这两个其实都可以实现我们加载的最终目的,但是他们一个是基于对象的,一个是属于面向对象的,就像是很多种情况,我们通过普通的编码也可以实现,但是我们引入设计模式来更好的体现编程思想
如果一个方法和他所在的类的实例对象确实是无关的,那么它就应该是静态的,反之它就应该是非静态的,如果我们需要使用非静态的方法,但是在创建类对象的时候,又只需要维护一个实例,不想创建多个不同的实例,就需要使用单例模式了
好了,以上就是全部内容了,我是小鱼仙,你们的学习成长小伙伴
我希望有一天能够靠写字养活自己,现在还在磨练,这个时间可能会有很多年,感谢你们做我最初的读者和传播者。请大家相信,只要给我一份爱,我终究会还你们一页情的。
再次感谢大家能够读到这里,我后面会持续的更新技术文章以及一些记录生活的灵魂文章,如果觉得不错的,觉得【大鱼同学】有点东西的话,求点赞、关注、分享三连
哦,对了!后续的更新文章我都会及时放到这里,欢迎大家点击观看,都是干货文章啊,建议收藏,以后随时翻阅查看
https://gitee.com/dayumm