专栏:设计模式-单例模式
Posted EAIPLUS
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了专栏:设计模式-单例模式相关的知识,希望对你有一定的参考价值。
图片 | Google
概念
java中单例模式是一种常见的设计模式,单例模式的写法有好几种,这里主要介绍两种:懒汉式单例、饿汉式单例。
单例模式有以下特点:
1、单例类只能有一个实例。
2、单例类必须自己创建自己的唯一实例。
3、单例类必须给所有其他对象提供这一实例。
单例模式确保某个类只有一个实例,而且自行实例化并向整个系统提供这个实例。在计算机系统中,线程池、缓存、日志对象、对话框、打印机、显卡的驱动程序对象常被设计成单例。这些应用都或多或少具有资源管理器的功能。每台计算机可以有若干个打印机,但只能有一个Printer Spooler,以避免两个打印作业同时输出到打印机中。每台计算机可以有若干通信端口,系统应当集中管理这些通信端口,以避免一个通信端口同时被两个请求同时调用。总之,选择单例模式就是为了避免不一致状态。
懒汉式非线程安全单例
/**
* @author sjt
* @Date 2020-04-07 00:59:04
*/
//懒汉非线程安全-第一次调用时实例化
public class Singleton {
private Singleton(){}
private static Singleton singleton = null;
public static Singleton getInstance(){
if(singleton == null){
singleton = new Singleton();
}
return singleton;
}
}
懒汉式线程安全单例
不了解并发编程的请移步饿汉单例,跳过此节,最后再看。
1. synchronized同步
/**
* @author sjt
* @Date 2020-04-07 00:59:04
*/
public class SecuritySingleton {
private SecuritySingleton(){}
private static SecuritySingleton singleton = null;
public synchronized static SecuritySingleton getInstance(){
if(singleton == null){
singleton = new SecuritySingleton();
}
return singleton;
}
}
解析
仅仅用synchronized修饰getinstance方法,就可以达到线程安全。
原因:synchronized修饰静态方法,作用范围是修饰的方法。
当多线程且执行到getinstance方法时,synchronized会锁住当前类,同一时刻只有一个线程可以执行getinstance方法,其他线程均阻塞。
更通俗的说,就是多线程操作时,每个线程都会执行代码块,也就是代码块会同时执行,那么不加synchronized线程同步的情况下,
可能new SecuritySingleton();会同时执行多次,得到返回不同的实例,造成并发错误。
也就是说,在多线程情况下,synchronized修饰的代码块会串行执行。
这样虽然保证了并发安全,但是同一时间只能执行一个线程,大大降低了效率。更可以说是这个锁的粒度太大了,并发效率和并发安全的权衡可以通过锁的粒度控制。
2. double-checked lock
private SecuritySingleton(){}
private volatile static SecuritySingleton singleton = null;
public static SecuritySingleton getInstance(){
if (singleton==null){
synchronized (SecuritySingleton.class){
if(singleton==null){
singleton = new SecuritySingleton();
}
}
}
return singleton;
}
解析
二次检测锁可以达到线程安全。
注意!singleton 静态类成员变量多加了volatile修饰,具体详见[2]。
以下都是极大多数情况,不包括极端例子。
简单讲就是多个线程同时执行到line 5,此时每个线程都认为singleton==null。
synchronized仍然使锁住的代码块串行执行,此时cpu调度某一个线程执行,其他线程阻塞。这个被调度的线程仍然认为Singleton==null,然后初始化得到一个实例。
假设new SecuritySingleton()正常执行完毕(绝大数情况),这样singleton赋值成功一个实例,对所有其他的线程均可见。也就表示所有线程的singleton都是同一个实例,成功单例化。
但是极少数情况new SecuritySingleton()会执行出错,得到一个不完整的实例。这样其他线程可能同步了一个不完整的实例,这就需要volatile来修正。
为什么new SecuritySingleton()会执行出错?
为什么volatile可以修正这个问题?
留一个小问题,希望你在后台留言给我答案。
3. 静态内部类
private SecuritySingleton(){}
private static class LazyHolder{
private static final SecuritySingleton INSTANCE = new SecuritySingleton();
}
public static final SecuritySingleton getInstance(){
return LazyHolder.INSTANCE;
}
3比1,2来说最好。
3不用同步减低效率,也不像2一样难理解和某些隐藏问题。
内部类初始化时就实例化singleton,天生线程安全。
加final修饰方法是若有继承关系,保证子类不能重写父类方法。
饿汉式线程安全单例
/**
* @author sjt
* @Date 2020-04-07 00:59:04
*/
//饿汉:类初始化就实例化,天生线程安全
public class Singleton {
private Singleton(){}
private static final Singleton singleton = new Singleton();
public static Singleton getInstance(){
return singleton;
}
}
饿汉式和懒汉式区别
从名字上来说,饿汉和懒汉,
饿汉就是类一旦加载,就把单例初始化完成,保证getInstance的时候,单例是已经存在的了,
而懒汉比较懒,只有当调用getInstance的时候,才会去初始化这个单例。
另外从以下两点再区分以下这两种方式:
1、线程安全:
饿汉式天生就是线程安全的,可以直接用于多线程而不会出现问题,
懒汉式本身是非线程安全的,为了实现线程安全有几种写法,分别是上面的1、2、3,这三种实现在资源加载和性能方面有些区别。
2、资源加载和性能:
饿汉式在类创建的同时就实例化一个静态对象出来,不管之后会不会使用这个单例,都会占据一定的内存,但是相应的,在第一次调用时速度也会更快,因为其资源已经初始化完成,
而懒汉式顾名思义,会延迟加载,在第一次使用该单例的时候才会实例化对象出来,第一次调用时要做初始化,如果要做的工作比较多,性能上会有些延迟,之后就和饿汉式一样了。
至于1、2、3这三种实现又有些区别,
第1种,在方法调用上加了同步,虽然线程安全了,但是每次都要同步,会影响性能,毕竟99%的情况下是不需要同步的,
第2种,在getInstance中做了两次null检查,确保了只有第一次调用单例的时候才会做同步,这样也是线程安全的,同时避免了每次都同步的性能损耗
第3种,利用了classloader的机制来保证初始化instance时只有一个线程,所以也是线程安全的,同时没有性能损耗,所以一般我倾向于使用这一种。
文末小问题
为什么在懒汉double-checked locking 方法中new SecuritySingleton()会执行出错?是怎样的一种并发错误?
为什么volatile可以修正这个问题?
可以参考文末[2]。
留一个小问题,希望你在后台留言给我答案。
参考资料
[1]. [23种设计模式汇总整理]
(https://blog.csdn.net/jason0539/article/details/44956775)
[2]. [double-check method潜在问题解决-volatile修饰解决]
(http://www.cs.umd.edu/~pugh/java/memoryModel/DoubleCheckedLocking.html)
还可以看
[设计模式汇总贴]
(https://shaojintian.cn/2020/04/07/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F%E6%B1%87%E6%80%BB%E8%B4%B4/)
如果本文有什么笔误、错误,请您在订阅号后台直接留言,作者看到后会及时勘误的!(蟹蟹)
点在看好不好,喵~
以上是关于专栏:设计模式-单例模式的主要内容,如果未能解决你的问题,请参考以下文章