问题思考
你知道什么是单例模式么?你能写出一个性能有保障并且安全的单例模式么?
首先我们先明确单例模式的概念,单例是指在全局范围内该类只存在一个实例。
这里的 "全局" 指的什么呢? 进程还是线程?
指的是进程,即java 虚拟机进程内是只有一个实例。
单例演进
一般了解java的同学都可以熟练的脱口而出,常用的两种单例模式,一种是饿汉式,一种是懒汉式
饿汉式
public class Singleton {
/**私有化无参数构造器*/
private Singleton(){}
/**声明为static final 常量*/
private static final Singleton INSTANCE = new Singleton();
/**获取实例*/
public static Singleton getInstance (){
return INSTANCE;
}
}
以上便是饿汉式单例的常规写法;那么如果反序列化对象,重新生成一个实例,那么在JVM中该类的实例是指向同一个内存地址么?这还是单例吗?
/**我们给以上类实现Serializable接口,执行以下代码*/
public static void main(String[] args) {
Singleton instance = Singleton.getInstance();
try {
/**将该对象序列化输出*/
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("single.obj"));
out.writeObject(instance);
out.flush();
out.close();
/**将该对象反序列化读入*/
ObjectInputStream in = new ObjectInputStream(new FileInputStream("single.obj"));
Singleton instance2 = (Singleton) in.readObject();
in.close();
System.out.println("instance 与 instance2 是同一个实例: " + (instance == instance2));
} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
执行结果是instance 与 instance2 是同一个实例: false,表明反序列化回来的与对象不是同一个实例;那么该如何保证反序列化回来仍为原实例呢?我们可以通过如下方式实现
在原有Single类中实现readResolve方法
public class Singleton implements Serializable {
/**私有化无参数构造器*/
private Singleton() {
}
/**声明为static final 常量*/
private static final Singleton INSTANCE = new Singleton();
/**获取实例*/
public static Singleton getInstance() {
return INSTANCE;
}
/**解决反序列化问题*/
public Object readResolve() {
return INSTANCE;
}
}
新增readResolve 方法后,我们再次执行main方法,发现返回的结果如下:
instance 与 instance2 是同一个实例: true
所以我们针对反序列化而产生的实例不唯一的问题都可以通过这种方式来解决,下文将不再赘述。
懒汉式
我们紧接着再回顾一下常用的懒汉式写法
public class Singleton {
/**私有化构造函数*/
private Singleton() {
}
/**声明私有 static 实例*/
private static Singleton instance;
/**声明公有的 获取实例方法*/
public static Singleton getInstance() {
if (null == instance) {
instance = new Singleton();
}
return instance;
}
}
以上便是懒汉式写法,但是这样的写法在高并发的情况下,没有同步机制的保证,是线程不安全的,有可能会存在多个实例;那么我们怎么升级一下呢?
懒汉式升级之同步锁版
public class Singleton {
/**私有化构造函数*/
private Singleton() {
}
/**声明私有 static 实例*/
private static Singleton instance;
/**声明公有的 获取实例方法 使用synchronized的关键字限制*/
public static synchronized Singleton getInstance() {
if (null == instance) {
instance = new Singleton();
}
return instance;
}
}
我们发现升级后的单例模式在getInstance方法上添加了synchronized关键字,实现了线程安全,那么在安全性能上没有什么问题,但是方法只能单线程访问,在效率上得不到保证;聪明的同学可能已经想到了双检锁的写法,我们一起来看看
懒汉式升级之上双检锁版
public class Singleton {
/**私有化构造函数*/
private Singleton() {
}
/**声明私有 static 实例*/
private static Singleton instance;
/**声明公有的 获取实例方法*/
public static Singleton getInstance() {
/**第一次检查*/
if (null == instance) {
synchronized(Singleton.class){
/**第二次检查*/
if (null == instance) {
instance = new Singleton();
}
}
}
return instance;
}
}
升级过后的我们的代码看上去完美无瑕,但是爱钻牛角尖的“独秀”同学可能会说,这个还是有一些问题,具体是什么问题呢我们一起来研究研究。
原来是这样的,在 instance = new Singleton();;JVM对于此处的操作并不是原子操作;具体分为三个步骤:
1.为instance对象开辟内存空间
2.调用构造函数初始化成员变量
3.将对象引用instance指向该内存空间(PS:此步骤后instance将不等于null)
由于此处的不是原子操作,JVM为了提升CPU的执行效率,会进行指令重排;造成这三步的执行顺序有可能为1-2-3、1-3-2;在后者发生的情况下,如果线程A步骤3执行之后,步骤2未执行之前;步骤2被线程B占用,这是instance不为null,但是却没有初始化;线程会正常返回,在后续的使用过程中由于没有初始化会造成出错。
那么针对这种情况,解决的思路也很明确,就是防止JVM指令重排,我们使用volatile关键字声明instance即可。
public class Singleton {
/**私有化构造函数*/
private Singleton() {
}
/**声明私有 static 实例,使用volatile 防止JVM指令重排*/
private static volatile Singleton instance;
/**声明公有的 获取实例方法*/
public static Singleton getInstance() {
/**第一次检查*/
if (null == instance) {
synchronized(Singleton.class){
/**第二次检查*/
if (null == instance) {
instance = new Singleton();
}
}
}
return instance;
}
}
虽然以上写法保证了线程安全;但是需要注意的是在JDK5之前使用双检锁volatile版本还是有问题的,因为在JDK5之前的JVM内存模型有缺陷,即使使用volatile关键字,也不能完全的防止指令重排;所以我们又有了其他版本的单例模式。
静态内部类写法
public class Singleton {
/**私有化构造函数*/
private Singleton() {
}
/**声明公有的 获取实例方法*/
public static Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
/**私有内部静态类*/
private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}
}
这种写法没有JDK版本的限制,并且静态内部类SingletonHolder是私有的,只有getInstance可以方法,所以它也是懒汉式的;同时读取时也没有限制,是线程安全的;这种方式也是《effective java》推荐的。
枚举单例写法
public enum Singleton{
INSTANCE;
}
这种写法简单,并且enum实现也是线程安全的,不需要担心双检锁;获取方式直接通过Singleton.INSTANCE,比getInstance简单;还可以防止反序列化。
延伸思考
最秀的永远是“独秀”同学,当我们通过反射的手段获取的对象实例跟原有实例是同一个实例么?为什么?
public static void main(String[] args) {
try {
SingletonReflect instance = SingletonReflect.getInstance();
Class clazz = Class.forName("xin.sunce.pattern.SingletonReflect");
Constructor constructor = clazz.getDeclaredConstructor(null);
constructor.setAccessible(true);
SingletonReflect instance2 = (SingletonReflect) constructor.newInstance();
System.out.println("instance 与 instance2 是同一个实例: " + (instance == instance2));
} catch (Exception e) {
e.printStackTrace();
}
}
执行结果是:
instance 与 instance2 是同一个实例: false
为此,我们可以通过一定的手段防止像“独秀”一样的同学走后门。
/**
* 静态内部类限制方式
* 修改私有构造函数,防止走后门
*/
private Singleton() {
if (null != SingletonHolder.INSTANCE) {
throw new RuntimeException();
}
}
/**
* 双检锁方式
* 修改私有构造函数,防止走后门
*/
private Singleton() {
if (null != instance) {
throw new RuntimeException();
}
}
读者可以根据实际场景结合具体需求来判断适合使用哪种单例模式,以及要对对应的单例模式做到心知肚明。
单例模式有什么优点呢?它又有什么缺点呢?
单例模式控制了只有一个实例,可以很好的避免资源重复加载,节省了内存空间;
但是只有一个实例,就对面向对象的特性支持有些欠缺;单例对代码的扩展性不友好,单例不支持有参数的构造函数。
总结
阅读完本文以后烦请你思考一下问题,有助于巩固理解:
1.思考你的单例是否是线程安全的
2.知晓你的单例在并发场景下会不会阻塞
3.你的单例经得起反序列化的考验么
4.你的单例经得起反射的考验么
- 本文链接: https://www.sunce.wang/archives/设计模式之你真的了解单例模式么
- 版权声明: 本博客所有文章除特别声明外,均采用CC BY-NC-SA 3.0 许可协议。转载请注明出处!