深入揭秘单例模式
单例模式是开发中频繁使用的设计模式之一,也是入门设计模式比较简单的一种设计模式。基本上开发者都会使用多种方式写单例模式,并且知道单例模式不同方式的优缺点。实际上,简单的单例模式也有其中的奥妙。
注意:本文重点讨论单例模式内部原理
1.懒汉式单例模式
懒汉式基本写法:
/**
* 懒汉式 单例模式
* @author jackcheng (jackcheng1117@163.com)
*/
public class LazySingleton {
private static LazySingleton instance = null;
private LazySingleton () {}
public static final LazySingleton getInstance() {
if (instance == null) {
instance = new LazySingleton();
}
return instance;
}
}
懒汉式特点:
1. 顾名思义比较“懒”’,反应到Java程序中就是实现了懒加载(Lazy Loader)
2.尽管将构造器置为private,但是通过反射依然可以创建出对象 (后续不再列出)
3. 多线程下有线程安全问题
所以A线程继续执行 instance = new LazySingleton(),开始初始化instance对象。当执行到第二 |
懒汉式单例模式在并发下创建多个对象,这完全违背了单例模式的初衷,解决方式如下:
/**
* 懒汉式 单例模式
* @author jackcheng (jackcheng1117@163.com)
*/
public class LazySingleton {
private static LazySingleton instance = null;
private LazySingleton () {
if (instance != null) {
throw new RuntimeException("单例构造器禁止使用");
}
}
public synchronized static final LazySingleton getInstance() {
if (instance == null) {
instance = new LazySingleton();
}
return instance;
}
}
这种方式没有什么安全问题,但是由于使用了synchronized锁,所以会影响性能。不推荐使用。
2.饿汉式单例模式
饿汉式单例模式基本写法:
/**
* 饿汉式 单例模式
* @author jackcheng (jackcheng1117@163.com)
*/
public class HungarySingleton {
private static final HungarySingleton INSTANCE = new HungarySingleton();
private HungarySingleton() {}
public static HungarySingleton getInstance() {
return INSTANCE;
}
}
饿汉式特点:
1. 没有实现懒加载
2. 多线程下线程安全
饿汉式除了没有实现懒加载,其他方面还是比较优秀的,开发中可以大胆使用。
3. Double Check Lock (双端检锁) 单例模式
基本写法:
/**
* Double Check Lock Singleton
* @author jackcheng (jackcheng1117@163.com)
*/
public class DoubleCheckLockSingleton {
private volatile static DoubleCheckLockSingleton instance = new DoubleCheckLockSingleton();
private DoubleCheckLockSingleton() {}
public static DoubleCheckLockSingleton getInstance() {
if (instance == null) {
synchronized(DoubleCheckLockSingleton.class) {
if (instance == null) {
instance = new DoubleCheckLockSingleton();
}
}
}
return instance;
}
}
双端检锁单例模式特点:
1. 实现懒加载
2.并发下无线程安全问题 (必须加 volatile关键字)
注意事项:(volatile关键字不再本文详细展开描述,有兴趣可以后续关注笔者文章)
使用双端检锁单例模式时,必须要加 volatile 关键字: 因为 instance = new DoubleCheckLockSingleton(); 对于单线程来说,进行指令重排不是影响最后的结果,而且可以进行优化。但是对于多线程来说,可能就会存在安全问题。
instance = new DoubleCheckLockSingleton();
若我们不加 volatile关键字,可能会进行指令重排,假设重排为: 1.给这个对象new DoubleCheckLockSingleton()分配内存
若重排为这种顺序,那么当执行到第二步时,就将instance指向了内存地址,但是此时 instance对象还没有完成初始化 但是此时第一个线程还没有完成对象初始化,第二个线程已经获得instance对象, 这样就会出现异常。
|
4. 静态内部类单例模式
基本写法:
/**
* 静态内部类 单例模式
* @author jackcheng (jackcheng1117@163.com)
*/
public class StaticInnerClassSingleton {
private StaticInnerClassSingleton() {}
public static StaticInnerClassSingleton getInstance() {
return InnerClass.instance;
}
private static class InnerClass {
public static StaticInnerClassSingleton instance = new StaticInnerClassSingleton();
}
}
静态内部类单例模式特点:
1. 实现懒加载
2. 并发下线程安全
解析:
静态内部类单例模式如何解决懒加载的呢? 首先了解一下一个类什么时候会立刻初始化呢? 由于没有直接new 对象,所以只会加载StaticInnerClassSingleton类,而不会进行初始化,进而实现了懒加载
静态内部类单例模式如何解决线程安全问题的呢? 在类的初始化的期间,JVM会去获取一个锁(class对象初始化锁),这个锁可以同步多个线程对一个类的初始化 。 不管是否重排序,对于线程1来说都是不可见的,所以 静态内部类可以解决 线程安全问题 |
5. 枚举单例模式
基本写法:
/**
* 枚举类 单例模式
* @author jackcheng (jackcheng1117@163.com)
*/
public enum EnumSingleton {
INSTANCE;
public static EnumSingleton getInstance() {
return INSTANCE;
}
}
枚举类单例模式特点:
1. 实现懒加载
2. 线程安全
枚举单例模式是比较完美的一种方式,在effictive java书中,作者也推荐使用这种方式。
通过jad反编译EnumSingleton,得到枚举类的构造器: private EnumSingleton(String s, int i) {
当我们试图通过利用反射获取EnumSingleton(String s, int i)这个构造器,发现会报错,说明反射是攻击不了的。 |
其他单例模式的实现方式不再列举(HashMap形式、ThreadLocal形式等)
6.避免克隆单例模式实例
/**
* 演示单例模式克隆案例
* @author jackcheng (jackcheng1117@163.com)
*/
public class Singleton implements Cloneable {
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
private Singleton() {
if (instance != null) {
throw new RuntimeException("单例构造器禁止反射调用");
}
}
private static Singleton instance = null;
private static boolean flag = false;
public static Singleton getInstance() {
if (instance == null) {
flag = true;
instance = new Singleton();
}
return instance;
}
static class Test {
public static void main(String[] args) throws CloneNotSupportedException {
Singleton instance = Singleton.getInstance();
Singleton cloneInstance = (Singleton) instance.clone();
System.out.println(instance); //com.ssm.test.Singleton@5a42bbf4
System.out.println(cloneInstance); //com.ssm.test.Singleton@270421f5
System.out.println(instance == cloneInstance); //false
}
}
}
当重写clone()方法后,克隆一份单例模式实例,测试发现,克隆的对象与单例实例竟然不是同一个对象
很显然违背了单例模式的初衷,如何避免呢?
将重写clone()方法改为 :
@Override
protected Object clone() throws CloneNotSupportedException {
return getInstance();
}
7. 避免序列化与反序列化造成不是同一个对象问题
将单例类实现序列化接口,然后对单例实例进行序列化操作,然后再进行反序列化操作。
发现序列化与反序列化之后竟然不是同一个对象,这不是也违背了单例模式的初衷吗?
为什么呢?
通过看源码发现,当进行反序列化时,会调用 ObjectInputStream.readObject()方法。
ObjectInputStream.readObject()是通过反射来反序列化对象的,既然是经过反射,那么肯定或获取一个新的 再new出来一个对象,自然和序列化时的对象是不一样的。 |
如何解决呢?
经过查看 ObjectInputStream.readObject()过程的源码,在进行反射获取对象时,会有一个判断, 这个判断就是判断要反序列化的类中有没有Object readResolve()方法 若有:则返回这个类的实例化对象(singleton) 若没有:则通过反射的newInstance()方法new一个实例,然后将这个实例返回 |
所以只需要在Singleton类中加一个方法即可:
private Object readResolve() {
return getInstance();
}
总结:
在使用单例模式时要注意以下几点:
1. 线程是否安全
2. 避免使用反射创建出对象
3. 避免克隆及克隆对象不一致问题
4. 避免序列化、反序列化时对象不一致问题
有兴趣的同学可以持续关注,或者有感兴趣的话题可以抛出来,一起探讨。