最近在测试的时候遇到几个关于多线程的问题,其实频繁涉及单例模式相关的问题,因此翻看文章和资料总结了本篇文档,做为后续的学习和工作参考。
一. 单例模式简介
单例模式是用来创建独一无二的,只有一个实例的对象的模式.使用单例模式创建一个模型,它确保只产生一个实例,并提供一个访问它的全局访问点。
1. 为什么使用单例模式?
部分对象使用过程中只能有一个实例,如果制造出多个,会导致很多问题,如程序的行为异常,资源使用过量和数据不一致的情况。
2. 静态变量实现单例的缺点
静态全局变量可以实现单例,但是这样必须在程序一开始就创建好对象,如果对象创建非常耗时,这会导致程序初始化过慢,同时如果执行过程中没有使用过,也会导致资源的浪费。
3. 单例模式应用场景
(1)资源共享的情况下,避免由于资源操作时导致的性能或损耗等。如日志文件,应用配置。
(2)控制资源的情况下,方便资源之间的互相通信。如线程池,缓存等。
二. 六种单例模式实现
1. 懒汉
线程不安全的写法。懒汉式是典型的以时间换空间,每次获取实例都会判断,看是否需要创建,浪费判断的时间。而如果没有人使用的话,就不会创建,节省了内存空间。1
2
3
4
5
6
7
8
9
10public class Singleton {
private static Singleton instance;
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
private Singleton (){}
}
线程安全的写法,但是效率很低1
2
3
4
5
6
7
8
9
10
11
12public class Singleton{
private static Singleton instance;
public static synchronized Singleton getinstance(){
if (instance == null){
instance = new Singleton();
}
return instance;
}
private Singleton(){}
}
2. 饿汉
类加载时直接初始化,如果不使用而提前创建会耗用性能。 饿汉式是典型的空间换时间,当类装载时会创建类的实例,不管你用不用,先创建出来,然后每次调用的时候不需要再判断,节省了运行时间。1
2
3
4
5
6
7
8public class Singleton {
private static Singleton instance = new Singleton();
public static Singleton getInstance() {
return instance;
}
private Singleton (){}
}
3. 双重校验锁
1 | class Helper { |
首先判断instance是不是为null,如果为null,加锁初始化;如果不为null,直接返回instance。
但是这个方法在单核和多核的cpu下都不能保证很好的工作。问题是由编译器优化导致。编译器优化是指在不改变原来语义的情况下,通过调整语句顺序,来让程序运行的更快。
回到问题,创建对象是非原子操作,会经历如下过程:
1,申请内存,调用构造方法初始化;
2,分配一个指针指向这块区域。
这两个操作JVM并没有规定谁先执行,编译器优化过程中就可能导致JVM是先开辟出一块内存,然后把指针指向这块内存,最后调用构造方法进行初始化。从而出现以下问题:
1、线程A进入getHelper()方法;
2、因为此时helper为空,所以线程A进入synchronized块;
3、线程A执行 helper = new Helper(); 先将helper指向一块内存,此时helper已经非空,而并未初始化;
4、同时线程B进入,检查helper不为空,则直接使用这个实例并使用,如果此时helper还为初始化完成就会出现问题。
为解决以上问题,有以下尝试:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23public class Singleton {
private static Singleton instance = null;
public static Singleton getInstance() {
if (instance == null) {
Singleton sc;
synchronized (Singleton.class) {
sc = instance;
if (sc == null) {
synchronized (Singleton.class) {
if(sc == null) {
sc = new Singleton();
}
}
instance = sc;
}
}
}
return instance;
}
private Singleton() { }
}
看起来这种方式可以解决问题,但是这种想法完全是错误的!同步块的释放保证在同步块里面的操作必须完成,但是并不保证同步块之后的操作不能因编译器优化而调换到同步块结束之前进行。因此,编译器完全可以把instance=sc;这句移到内部同步块里面执行。这样,程序又是错误的了!
4. 双重校验锁问题解决方案
在JDK 5之后,Java使用了新的内存模型。volatile关键字有了明确的语义——在JDK1.5之前,volatile是个关键字,但是并没有明确的规定其用途——被volatile修饰的写变量不能和之前的读写代码调整,读变量不能和之后的读写代码调整!因此,只要我们简单的把instance加上volatile关键字就可以了。关于volatile关键字分析可参考文章Java并发编程:volatile关键字解析。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15class Helper {
private volatile Helper helper = null;
public Helper getHelper() {
Helper hp;
if (hp == null) {
hp = helper;
synchronized(this) {
if (helper == null)
helper = new Helper();
}
}
return helper;
}
}
5. 内部类实现
1 | public class Singleton{ |
这种方式同样利用了classloder的机制来保证初始化instance时只有一个线程,它跟饿汉方式不同的是:饿汉方式是只要Singleton类被装载了,那么instance就会被实例化(没有达到lazy loading效果),而这种方式是Singleton类被装载了,instance不一定被初始化。因为InnerSingleton类没有被主动使用,只有显示通过调用getInstance方法时,才会显示装载InnerSingleton类,从而实例化instance。
想象一下,如果实例化instance很消耗资源,我想让他延迟加载,另外一方面,我不希望在Singleton类加载时就实例化,因为我不能确保Singleton类还可能在其他的地方被主动使用从而被加载,那么这个时候实例化instance显然是不合适的。
6. 枚举实现
1 | public enum Singleton{ |
这种方式是Effective Java作者Josh Bloch 提倡的方式,它不仅能避免多线程同步问题,而且还能防止反序列化重新创建新的对象
参考文档: 单例模式的七种写法 , 深入Java单例模式 , head first设计模式