什么是单例模式
保证整个系统中一个类只有一个实例,并且提供一个可以全局访问的入口,实现这种功能的方式就叫做单例模式
为什么要使用单例模式
节省公共资源
一些对象频繁的创建和销毁会非常耗费性能和资源,比如,频繁访问数据库或文件的对象(DruidDataSource、SqlSessionFactory)
这些操作往往很费时,如:查询数据库并对查到的数据做大量计算、加解密、解压缩和读取文件等
解决在多线程下资源访问冲突的情况
以日志管理为例:
1
2
3
4
5
6
7
8
9
10
11
12
public class Logger {
private FileWriter writer;
public Logger() {
File file = new File("/path/log.txt");
writer = new FileWriter(file, true); //true表示追加写入
}
public void log(String message) {
writer.write(message);
}
}
单例模式配合线程安全的 writer(),能避免资源访问冲突问题
如果 Logger 不是单例,那么在多个线程同时操作文件 “/path/log.txt” 时可能出现线程安全问题,如下图所示:
有些数据在内存中只需保存一份,如配置信息类、Class<?> 类
如何实现一个单例
思路:
- 构造器私有
- 静态方法返回实例
- 是否支持延迟加载 ?懒汉 :饿汉
饿汉模式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
/**
* 声明时初始化
*/
public class SingletonDemo1 {
private final static SingletonDemo1 INSTANCE = new SingletonDemo1();
public static getInstance(){
return INSTANCE;
}
}
/**
* 静态代码块初始化
*/
public class SingletonDemo2 {
private final static SingletonDemo2 INSTANCE;
static {
INSTANCE = new SingletonDemo2();
}
public static getInstance(){
return INSTANCE;
}
}
/**
* 巧用枚举,当只有一个枚举实例时,其实就是单例模式
*/
public enum SingletonDemo3 {
INSTANCE;
}
上面的单例对象都是在类加载时就已经被创建了,所以不存在线程安全问题
懒汉模式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
/**
* 虽然是内部类但也是独立的,只有在使用到时才会加载
*/
public class SingletonDemo6 {
private SingletonDemo6(){}
/**
* 静态内部类相对于外部类是独立的,只有在被使用时或者方法被调用时会被加载
*/
private static class Inner{
private static SingletonDemo6 INSTANCE = new SingletonDemo6();
}
public static SingletonDemo6 getINSTANCE() {
return Inner.INSTANCE;
}
}
/**
* 双重检测(提高并发量)
*/
public class SingletonDemo6 {
private static volatile SingletonDemo6 INSTANCE;
private SingletonDemo6(){}
public static SingletonDemo6 getINSTANCE() {
//最外层检测避免每个线程都去获得/释放 class 的锁(耗时操作)
if(INSTANCE == null){
synchronized(SingletonDemo6.class) {
if(INSTANCE == null){
INSTANCE = new SingletonDemo6();
}
}
}
return INSTANCE;
}
}
双重检测中的 volatile
在双重检查锁模式中为什么需要使用 volatile 关键字
new SingletonDemo6() 实际不是一个原子操作,在 JVM 中这条语句至少做了下面这 3 件事:
- 给 INSTANCE 分配内存空间
- 调用构造器初始化 INSTANCE
- 将 INSTANCE 对象指向分配的内存空间(执行完这步 INSTANCE 就不是 null 了)
但是由于指令重排优化,创建对象的顺序不一定是 1 -> 2 -> 3,也可能是 1 -> 3 -> 2,这样有个问题:对象还未初始化,却已经不为空了,这样双重检查就会失效,那么 INSTANCE 的使用就会报错,具体如下图:
总结
使用了 volatile 之后,相当于是表明了该属性的更新可能是在其他线程中发生的,而 volatile 的意义主要在于它可以避免拿到没完成初始化的对象,从而保证了线程安全。