先上答案,懒加载的最佳实践
public class Singleton {
private static class SingletonHolder {
private static Singleton singleton = new Singleton();
}
private Singleton() {
}
public static Singleton getSingleton() {
return SingletonHolder.singleton;
}
}
// 防new,防反射,防序列化,并且也是懒加载
public enum MySingleton3 implements MySingleton {
SINGLETON,
;
public static MySingleton3 getInstance(){
return SINGLETON;
}
}
最佳实践的起源
在没有内部类的情况下,类的初始化顺序为:
类的静态成员变量【类所有】-->静态代码块-->成员变量【对象所有】-->代码块-->构造函数实例化【对象所有】
依据单例的性质,只持有一个实例,外部无法使用构造函数进行初始化,很容易想到以下实现方式1。
public class Singleton1 {
private static Singleton1 singleton;
private Singleton1() {
}
public static Singleton1 getSingleton() {
if (null == singleton) {
singleton = new Singleton1();
}
return singleton;
}
}
实现方式1,在单线程场景下使用没有问题,但是在并发场景下:多个线程同时调用getSingleton( ),并判断 null == singleton ,依然会造成多次实例化,带来并发场景下的线程安全问题,很容易想到加锁的方式来保证线程安全。
单例模式实现方式2
public class Singleton1 {
private static Singleton1 singleton;
private Singleton1() {
}
public synchronized static Singleton1 getSingleton() {
if (null == singleton) {
singleton = new Singleton1();
}
return singleton;
}
}
实现方式2 的这种加锁方式,粒度太大【类锁,其实是xxx.class作为锁,在同步方法时加锁】,并且当getSingleton( )方法高频,高并发场景下多线程同时等待获取锁进入方法,在该处由并行变成串行,性能下降。
由于实现方式2中的加锁粒度过大,导致性能下降,可以减小锁的粒度,于是有了double check lock的方式
单例模式实现方式3
public class Singleton1 {
private static Singleton1 singleton;
private Singleton1() {
}
public static Singleton1 getSingleton() {
if (null == singleton) { // 1
synchronized (Singleton1.class) { // 2
if (null == singleton) { // 3
singleton = new Singleton1(); // 4 实例化
}
}
}
return singleton;
}
}
实现方式3看起来完美,但是忽略了一个问题:就是JVM 通过 new 实例化对象的时候【上述代码中步骤4】,可能会进行指令重排。比如:
其实是有三个步骤的:
memory = allocate(); // 指令1:分配对象的内存空间
ctorInstance(memory); // 指令2:初始化对象
instance = memory; // 指令3:设置instance指向刚分配的内存地址
JVM 为了性能方面的考虑,可能会进行指令重排,比如,指令3 会在 指令2 之前执行,导致了实现方式3 在步骤 3 进行 if (null == singleton)的判断时,singleton 并没有初始化完成而直接返回了一个空对象,引发问题。
单例懒加载实现方案4:
public class Singleton1 {
private static volatile Singleton1 singleton;
private Singleton1() {
}
public static Singleton1 getSingleton() {
if (null == singleton) {
synchronized (Singleton1.class) {
if (null == singleton) {
singleton = new Singleton1();
}
}
}
return singleton;
}
}
volatile关键字,无法保证线程安全【即无法保证操作的原子性】,但是可以防止指令重排【保证操作有序性】,并且能够保证volatile关键字修饰的共享变量的可见性【即,线程A操作完之后会将操作后的值刷回主存,另一个线程B识别到该共享变量,不会从线程B的CPU高速缓存中读取,而从主存中直接读取】,加上synchronized关键字,可以保证线程安全性。下图是volatile 和 synchronized 关键字的语义。
需要详细理解volatile关键字,请参考 https://blog.csdn.net/justloveyou_/article/details/53672005
单例懒加载实现5:
类的加载机制保证: 引用<深入理解JVM>
1、遇到 new、getstatic、setstatic 或 invokestatic 这 4 个字节码指令时,分别对应如下Java代码场景:
new : new一个实例化对象
getstatic 读取一个静态字段(final修饰、已在编译期把结果放入常量池的除外)
setstatic 设置一个静态字段(同上)
invokestatic 调用一个类的静态方法
2、使用 java.lang.reflect 包中的方法,对类进行反射调用时,如果类没有初始化过,会触发初始化之。
3、当初始化一个类时,如果父类未初始化,会先触发父类的初始化。
4、当虚拟机启动时,用户需要制定一个要执行的主类(含 main 方法的类),虚拟机会先初始化这个类。
5、当使用JDK 1.7等动态语言支持时,如果一个java.lang.invoke.MethodHandle
实例最后的解析结果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,
并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化。
以上5种情况属于主动引用,其他情况下属于被动引用。
静态内部类就是被动引用的类型。即,内部类的加载不会随着外部类的加载而加载,而是在使用的时候进行加载。
当 getInstance() 方法被调用时,SingletonHolder 才在 Singleton 的运行时常量池里,把符号引用替换为直接引用,这时静态对象 singleton 也才最终被创建,然后再被 getInstance() 方法返回。
<深入理解JVM> 引用
JVM 保证一个类的 <clinit>() 方法在多线程环境中被正确地加锁、同步。
如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的<clinit>() 方法,
其他线程都需要阻塞等待,直到活动线程执行完 <clinit>() 方法。
当某个线程第一次执行了<clinit>() 这个初始化方法之后,其他线程就不会重复执行初始化了。
同一个加载器下,一个类只会被初始化一次。
由以上两点,就可以保证单例的延迟加载和线程安全。
单例懒加载实现方式5
public class Singleton {
private static class SingletonHolder {
private static Singleton singleton = new Singleton();
}
private Singleton() {
}
public static Singleton getSingleton() {
return SingletonHolder.singleton;
}
}
说到了类的加载机制,考虑到enum的特性,便有了单例实现方式6
单例懒加载实现方式6:
// 防new,防反射,防序列化
public enum MySingleton3 {
SINGLETON,
;
public static MySingleton3 getInstance(){
return SINGLETON;
}
}
枚举类里面的对象都是static final 的成员变量,final修饰的成员变量,不会触发实例初始化。枚举的初始化时机,在引用枚举的时候,在静态代码块里面初始化的【这个可以通过反编译看出来】,并且,只会只会实例化一次。所以是懒加载,并且是线程安全的。
Week 枚举类反编译示例代码
public final class Week extends Enum
{
private Week(String s, int i)
{
super(s, i);
}
public static Week[] values()
{
Week aweek[];
int i;
Week aweek1[];
System.arraycopy(aweek = ENUM$VALUES, 0, aweek1 = new Week[i = aweek.length], 0, i);
return aweek1;
}
public static Week valueOf(String s)
{
return (Week)Enum.valueOf(learnbymaven/single/Week, s);
}
public static final Week Monday;
public static final Week Tuesday;
private static final Week ENUM$VALUES[];
// 静态代码块初始化变量
static
{
Monday = new Week("Monday", 0);
Tuesday = new Week("Tuesday", 1);
ENUM$VALUES = (new Week[] {
Monday, Tuesday
});
}
}
鸣谢
感谢BenYoung 对于类的加载机制的总结:https://juejin.cn/post/6844903877326667789
感谢书呆子Rico 对于volatile的关键字的详细总结:https://blog.csdn.net/justloveyou_/article/details/53672005