说明
// 1.双重检验,单例模式,懒汉式,保证线程安全
实现
// #.final修饰,避免子类继承,覆盖父类方法,破坏单例
public final class Singleton implements Serializable{
// #.私有构造方法,避免被外面使用,但无法避免反射构造实例
private Singleton(){}
// #.volatile修饰避免指令重排序,读写屏障
private static volatile Singleton instance;
public static Singleton getInstance(){
//#.第一个if判断是否为空,不为空直接返回,避免synchronized同步代码块的执行,多线程场景下频繁加锁会影响性能
if(instance == null){
// 首次访问会同步,之后的使用没有synchronized
synchronized (Singleton.class){
// #.第二个if判断是否为空,当a线程优先获得锁,执行到此处,b线程没竞争到锁会被阻塞在外面,a线程判断实例是否为空,为空则new实例,a线程释放锁之后,b线程拿到锁进来后判断instance是否为null,此时不为null,则释放锁往下
if(instance == null){
instance = new Singleton();
}
}
}
return instance;
}
// 如果实现了序列化接口,加这个方法来防止反序列化破坏单例
public Object readResolve(){
return instance;
}
}
三、getInstance字节码分析
0 getstatic #2 // 获取静态变量instance
3 ifnonnull 37 // instance不为null,跳到37行
6 ldc #3 // 加载类对象
8 dup // 复制类对象地址
9 astore_0 // 存储一份类对象,解锁用
10 monitorenter // 进入synchronized同步代码块,创建monitor对象,monitor对象是否有owner,没有则成为owner,有则进入阻塞队列
11 getstatic #2 // 获取静态变量instance
14 ifnonnull 27 // instance不为null,则跳到27行
17 new #3 // 创建instance实例对象,将对象引入栈, new Singleton()
20 dup // 复制instance实例对象的一个引用地址
21 invokespecial #4 // 通过复制的引用调构造方法,根据复制的引用地址调用构造方法
24 putstatic #2 // 赋值给静态变量instance,利用一个对象引用地址,赋值给static instance
27 aload_0 // 加载类对象
28 monitorexit // 解锁
29 goto 37 // 跳到37行
32 astore_1
33 aload_0
34 monitorexit
35 aload_1
36 athrow
37 getstatic #2 // return instance 获取静态变量instance
40 areturn // return instance return结束
四、不用volatile修饰产生的问题
- 1.当线程1执行17行new了一个实例对象的地址,执行20行dup复制了instance实例对象的地址
- 2.线程1执行21行invokespecial,通过复制的引用地址调构造方法,由于构造方法时间过长,还未完成,指令重排,先执行24行赋为构造好的给实例对象,后调构造方法
- 3.线程2执行到0行,获取静态变量instance,执行3行ifnonnull,由于线程1已经new出了实例对象的地址,因此ifnonnull指令的结果为非空成立,则将未构造好的对象返回了出去
五、用volatile修饰
- 1.当线程1执行17行new了一个实例对象的地址,执行20行dup复制了instance实例对象的地址
- 2.线程1执行21行invokespecial,通过复制的引用地址调构造方法,执行24行putstatic,是赋值写操作,会带写屏障,写屏障的作用是写操作之前的指令不会重排,即21行调用构造方法的指令不会在24行赋值指令之后执行,确保了先构造后赋值的顺序
- 3.线程2执行到0行,获取静态变量instance,此时instance是构造好的对象,不为空,则返回
- 4.在线程1执行完21行调用构造方法的指令之后,还未执行24行赋值指令时,线程2此时执行到0行,获取静态变量instance,此时主存中的静态变量的实例是null,则进入synchronized中阻塞等待,等线程1执行完后,线程2才能拿到锁继续判断,此时不为null,则返回该实例对象