p32 面试题2:实现Singleton 模式面试
/**
* 多线程高效单例 Double Check
* @Author: hebin.yang
* @CreateDate: 2019-06-30 11:35
*/
public class Ex2Singleton3 {
private static Ex2Singleton3 instance;
private Ex2Singleton3() {}
/**
* Double Check
* 分析:因为 synchronized 关键字能够确保在释放锁以前,将数据刷新到主内存中,线程2 能够读取到最新的值
* 从而确保不会重复建立对象。
* 缺陷:无序性
* new 一个对象分两步,并且是无序的。
* @return
*/
public static Ex2Singleton3 getInstance() {
if (instance == null) {
synchronized (Ex2Singleton3.class) {
if (instance == null) {
instance = new Ex2Singleton3();
}
}
}
return instance;
}
}
复制代码
/**
* 安全版 Double Check 单例模式
* @Author: hebin.yang
* @CreateDate: 2019-06-30 16:02
*/
public class Ex2Singleton4 {
/**
* 增长 volatile 修饰,禁止建立对象指令重排序,修复多线程下因为指令重排序致使的异常
*/
private static volatile Ex2Singleton4 instance;
private Ex2Singleton4() {}
public static Ex2Singleton4 getInstance() {
if (instance == null) {
synchronized (Ex2Singleton4.class) {
if (instance == null) {
instance = new Ex2Singleton4();
}
}
}
return instance;
}
}
复制代码
关于 volatile 指令重排序的分析,详见如下文章:安全
Double check 为什么须要 volatile?bash
剑指Offer 说 double check 方法有点复杂容易出错实在有点牵强。多线程
特性:利用在 Java 中,静态内部类只会在第一次调用到时进行初始化的特性,实现单例模式。post
/**
* 静态内部类单例模式
* @Author: hebin.yang
* @CreateDate: 2019-06-30 16:33
*/
public class Ex2Singleton5 {
static class SingletonHolder {
private static Ex2Singleton5 INSTANCE = new Ex2Singleton5();
}
private Ex2Singleton5() {}
public static Ex2Singleton5 getInstance() {
return SingletonHolder.INSTANCE;
}
}
复制代码
分析:当Ex2Singleton5 加载时,并不会实例化静态内部类,只有当第一次调用静态内部类的 INSTANCE 时,才会加载 SingletonHolder,从而实例化 Ex2Singleton5 这个单例的对象。性能
优点:因为这种场景下单例对象的实例化,是在完成静态内部类加载时完成的,因此天生对多线程友好。getInstance() 也不须要关键字来同步。ui
优势:延迟加载,无需增长同步关键字(减小性能损耗)。spa
原理线程
因为静态内部类加载时只会执行一次类初始化方法 <clinit>()
,JVM 对该方法加了锁,确保当多个线程同时试图去执行类的初始化方法 <clinit>()
时,只有一个线程能执行,其他线程阻塞。而且,确保该方法只被执行一次。code
因此实现了线程安全。
总结:一个静态内部类被加载初始化时,从类加载器的角度来看,是线程安全的,而单例对象 INSTANCE 在这个静态内部类中是一个静态 Field,因此它跟随这个静态内部类的加载而初始化,且只在类加载时初始化一次。
详细分析可参考:《类加载时机汇总》