版权声明
版权声明:本文为博主原创文章,转载请注明出处+地址
前言
相信各位对单例模式都不陌生,这已经是一个老生常谈的设计模式。之前我对于单例模式的理解仅仅停留在表面上,知道有几种,知道如何实现,知道大概的区别如何,但是其实单例模式还有很多不为人知的另一面,接下来我们就一起来看看。
单例模式的五种写法
方案一: 饿汉式
public class Singleton {
private static Singleton = new Singleton();
private Singleton() {}
public static getSignleton(){
return singleton;
}
}
方案二:懒汉式
public class Singleton {
private static Singleton singleton = null;
private Singleton(){}
public static Singleton getSingleton() {
if(singleton == null) singleton = new Singleton();
return singleton;
}
}
方案三:线程安全
public class Singleton {
private static volatile Singleton singleton = null;
private Singleton(){}
public static Singleton getSingleton(){
synchronized (Singleton.class){
if(singleton == null){
singleton = new Singleton();
}
}
return singleton;
}
}
方案四:双重校验锁
public class Singleton {
private static volatile Singleton singleton = null;
private Singleton(){}
public static Singleton getSingleton(){
if(singleton == null){
synchronized (Singleton.class){
if(singleton == null){
singleton = new Singleton();
}
}
}
return singleton;
}
}
方案五:静态内部类
public class Singleton {
private static class Holder {
private static Singleton singleton = new Singleton();
}
private Singleton(){}
public static Singleton getSingleton(){
return Holder.singleton;
}
}
方案六:枚举
public enum Singleton {
INSTANCE;
}
单例模式的线程安全
方案一和方案二,非线程安全;
方案三和方案四,将唯一的实例进行判空操作,以及实例化的部分进行了synchronized进行加锁,实现了部分线程安全,那么还有什么隐患呢?那就是指令重排优化。
这里我们着重讲一下volatile修饰符,
volatile
volatile,有两层含义。第一层,可见性。第二层,防止重排序。
在类加载的过程中,分为以下几个步骤:
- 分配内存空间
- 初始化
- 将 singleton 对象指向分配的内存地址
在没有volatile修饰之前,初始化和指向分配内存地址的执行顺序不可预测,那么就会导致一个问题:
在线程A正在进行类加载的过程中,恰好先执行了指向分配内存地址这一步,此时singleton已经不为null,但是由于还没有初始化,所以singleton也不能算是一个正常的实例对象。这个时候切换到线程B,由于singleton此时已经不是null,所以会直接返回,拿去调用一些方法,这个时候就会抛出异常。
volatile是如何保证线程安全的呢?
那么volatile的出现保证了在类加载的过程中,先进行初始化,再将singleton对象指向分配的内存地址。
在线程A进行类加载的时候,假如执行到初始化这一步,线程B的请求过来,由于此时singleton是null的,所以会被synchronized同步锁锁住,直到singleton实例化成功,再进行调用。
方案五,将实例放置在静态内部类中,静态内部类只会被加载一次,实现了线程安全。
方案六,枚举的内部实现自动帮我们实现了线程安全。
单例模式的反射安全
针对除了枚举单例以外的方案来说,为什么会有反射不安全呢?如果用户通过类的反射去调用构造函数,获取实例,那么我们的单例就不是真正意义上的单例模式了,那么如何解决这个问题呢?
在构造函数中,对singleton判空,如果非空,那我们就抛出异常,防止其反射调用。
【枚举自带防止反射强行调用的构造器】
单例模式的序列化安全
针对singleton实例的序列化和反序列化得到的对象是新的对象,那么这样就破坏了singleton的唯一性。
那么为什么在序列化的过程中会生成新的对象呢?
因为序列化会通过反射调用无参构造函数创建一个新的对象,如何避免呢?我们就回到了上一个问题,怎么去规避反射调用singleton的构造函数。
【枚举单例实现了自动序列化机制,防止了反序列化的时候创建新的对象】
总结
单例的四大要点:
- 线程安全
- 反射安全
- 序列化与反序列化安全
- 延迟加载