单例模式
单例模式
保证一个类在内存中只有一个实例,并提供一个全局访问点
- 饿汉式
1 | class Singleton{ |
- 懒汉式
1
2
3
4
5
6
7
8
9
10
11
12class Singleton{
private static Singleton s; //懒汉模式,声明时未实例化
private Singleton(){}
public static Singleton getInstance(){
if(s == null) //存在线程安全问题
s = new Singleton();
return s;
}
}
未实例化时,可能存在多个线程都通过判空s==null的情况,因此存在线程安全问题。
- 双重检查加锁
改进懒汉式线程安全问题,在创建对象前加同步锁Synchronized
1 | class Singleton{ |
第一次判空防止实例化后,每次调用getInstance方法都会同步,提高性能
第二次判空防止未实例化时,多个线程通过第一次判断,进入等待。其中一个线程创建了对象后,另一个线程也能再一次创建对象,无法保证单例
volatile关键字防止因为JVM的指令重排而产生线程安全问题,保证指令执行的顺序
如:instance = new Singleton,会被编译器编译成如下JVM指令:
memory = allocate(); //1:分配对象的内存空间
ctorInstance(memory); //2:初始化对象
instance = memory; //3:设置instance指向刚分配的内存地址但是这些指令顺序并非一成不变,有可能会经过JVM和CPU的优化,指令重排成下面的顺序:
memory = allocate(); //1:分配对象的内存空间
instance = memory; //3:设置instance指向刚分配的内存地址
ctorInstance(memory); //2:初始化对象当线程A执行完1,3时,instance对象还未完成初始化,但已经不再指向null。此时如果线程B抢占到CPU资源,执
行if(instance == null)的结果会是false,从而返回一个没有初始化完成的instance对象。加入volatile关键字时,生成的汇编代码会多出一个lock前缀指令,lock前缀指令实际上相当于一个内存屏障(也叫内存栅栏),内存屏障会提供3个功能:
1.它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面,即在执行到内存屏障这句指令时,在它前面的操作已经全部完成;
2.它会强制将对缓存的修改操作立即写入主存;
3.如果是写操作,它会导致其他CPU中对应的缓存行无效
volatile不保证原子性,保证可见性和部分有序性
- 静态内部类
1
2
3
4
5
6
7
8
9
10
11public class Singleton {
private Singleton() {}
public static Singleton getInstance(){
return SingletonHolder.instance;
}
private static class SingletonHolder{
private static Singleton instance = new Singleton();
}
}
instance对象是通过在调用getInstance方法时才实例化对象,通过ClassLoader的加载机制实现懒加载(加载外部类时,并不加载内部类)
线程安全,但能利用反射破坏单例
1 | //获得构造器 |
- 单元素枚举
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16public enum ResourceEnum {
INSTANCE;
private Resource instance;
ResourceEnum() {
instance = new Resource();
}
public Resource getInstance() {
return instance;
}
}
// 需要单例的资源
class Resource{
}
调用方法:ResourceEnum.INSTANCE.getInstance()
枚举类的构造方法限制为私有,在访问枚举实例时会执行构造方法
每个枚举实例都是static final类型,只能被实例化一次
单例对象在枚举类加载时进行初始化,并非懒加载
能防止反射破坏单例
保证反序列化结果为同一对象
JVM在序列化时,只是将枚举对象的name属性输出到结果中,反序列化时通过java.lang.Enum的valueOf()方法根据名字查找枚举对象。因此反序列化后的对象和序列化前的对象实例相同。
其他单例模型要做到这点,必须实现readResolve()方法