逐步构建一个“铜墙铁壁”的单例模式
Posted 海涛技术漫谈
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了逐步构建一个“铜墙铁壁”的单例模式相关的知识,希望对你有一定的参考价值。
单例模式被公认为是设计模式中最简单的一种,用于保证系统中,某个类只有一个实例,运用非常广泛。
单例模式,往简单了说,其实关键就是,控制构造函数的访问权限,然后对外提供统一的访问点。
但其实,写好一个单例模式,并没有大家想的那么简单。下面,通过一步步的迭代优化,从线程安全和防破坏两个维度,逐步的实现一个“铜墙铁壁”般牢固的单例模式demo。
主要包含如下几块内容:
普通饱汉式和饿汉式
线程安全的饱汉式 (加锁和DCL)
静态内部类方式
单例模式的破坏
终极大法:枚举
一把无坚不摧的矛:Unsafa类
总结
一:普通实现方式
普通的饱汉式和饿汉式单例模式实现,应该是大家接触的最多的实现方式,他们实现简单,便于理解。
饱汉式的优势在于懒加载,对于非常消耗资源,占内存的对象尤其有效,但其实线程不安全的。
而饿汉式的优势在于,其天生的线程安全性。因此在大多数的场景下,饿汉式的单例模式已经够用。
二:线程安全的饱汉式
第一节讲了,饱汉式的实现方式是线程不安全的,因为它的非空判断和初始化是多步操作,不是原子的。
最简单的方式,通过synchronize关键字直接给方法加锁,但这样的方式比较低效,比较简单,这里就不过多阐述了。
更好的方式是DCL (Double Check Lock),双重check机制。第一个check是为了对象已经创建后,产生不必要的同步。第二个check,是避免第一个判空之后,进入同步方法前,有其他线程创建了实例。
需要注意的是,instance这个属性的volatile关键字,因为实例的创建不是原子操作,它包含了:(1) 分配内存,(2) 初始化对象,(3) 引用指向新的内存空间 三个步骤,其中2依赖于1,但是3不依赖于2,所以由于CPU指令重排序的影响,其他线程可能看到的是“半个”对象,而加上volatile关键字就是为了避免指令重排序。
public class TestSingleton {
public static void main(String[] args) {
// 多线程环境下,创建实例
final Map<String, HungrySingleton> HungryMap = new ConcurrentHashMap(10);
final Map<String, FullSingleton> FullMap = new ConcurrentHashMap(10);
final Map<String, DCL> DCLMap = new ConcurrentHashMap(10);
for (int i = 0; i < 1000; i++) {
final int threadIndex = i;
new Thread(new Runnable() {
public void run() {
HungryMap.put("thread" + threadIndex, HungrySingleton.getInstance());
FullMap.put("thread" + threadIndex, FullSingleton.getInstance());
DCLMap.put("thread" + threadIndex, DCL.getInstance());
}
}).start();
}
// 通过set的size大小,来判断是否创建了不同的实例
Set<HungrySingleton> hungrySingletonSet = new HashSet<HungrySingleton>();
hungrySingletonSet.addAll(HungryMap.values());
System.out.println("饿汉式单例多线程下是否产生了不同的对象:" + (hungrySingletonSet.size() > 1));// 偶尔会为:true
Set<FullSingleton> FullSingletonSet = new HashSet<FullSingleton>();
FullSingletonSet.addAll(FullMap.values());
System.out.println("饱汉式单例多线程下是否产生了不同的对象:" + (FullSingletonSet.size() > 1)); // 一直 false
Set<DCL> dclSingletonSet = new HashSet<DCL>();
dclSingletonSet.addAll(DCLMap.values());
System.out.println("DCL式单例多线程下是否产生了不同的对象:" + (dclSingletonSet.size() > 1)); // 一直 false
}
}
class HungrySingleton {
private static HungrySingleton instance = null;
private HungrySingleton(){
}
public static HungrySingleton getInstance() {
if (instance == null) {
instance = new HungrySingleton();
}
return instance;
}
}
class FullSingleton {
private static final FullSingleton instance = new FullSingleton();
private FullSingleton(){
}
public static FullSingleton getInstance() {
return instance;
}
}
class DCL {
private static volatile DCL instance = null;
private DCL() {
}
public static DCL getInstance() {
if (instance == null) {
synchronized (DCL.class) {
if (instance == null) {
instance = new DCL();
}
}
}
return instance;
}
}
代码1:多线程环境下的单例模式
三:静态内部类方式
说完了饱汉式和饿汉式,那么,有没有哪种方式可以结合两者的优点呢?既能实现懒加载,又能线程安全。
通过静态内部类就能实现这一要求。静态内部类和其外部类没有啥太多的必然联系,可以看成连个独立的类,外围类的加载不会触发静态内部类的类加载,只有调用静态内部类的静态变量时,才会触发类加载。
class InnerClassSingleton {
private static class SingletonHolder{
private static final InnerClassSingleton instance = new InnerClassSingleton();
private SingletonHolder(){
}
}
private InnerClassSingleton() {
}
public static InnerClassSingleton getInstance() {
return SingletonHolder.instance;
}
}
代码2:静态内部类实现方式
四:单例模式的破坏
上诉的方式,看上去貌似完美的实现了单例模式,既能做到线程安全,又能实现懒加载,但他们都是基于一点:私有的构造函数。
这就意味着,上诉方式实现的单例都能通过反射或者序列化进行破坏。示例代码如下,所有的输出均为false。
public class Code3 {
public static void main(String[] args) throws Exception {
/* 测试饿汉式 */
HungrySingleton hungryOrigin = HungrySingleton.getInstance();
// 反射
Class clazz = Class.forName("zhanht.HungrySingleton");
Constructor[] constructors = clazz.getDeclaredConstructors();
constructors[0].setAccessible(true);
HungrySingleton hungryReflect = (HungrySingleton) constructors[0].newInstance();
System.out.println(hungryOrigin == hungryReflect);
// 反序列化
String jsonStr = JSON.toJSONString(hungryOrigin);
HungrySingleton hungryJson = JSON.parseObject(jsonStr, HungrySingleton.class);
System.out.println(hungryOrigin == hungryJson);
/* 测试饱汉式 */
FullSingleton fullOrigin = FullSingleton.getInstance();
// 反射
Class clazzFull = Class.forName("zhanht.FullSingleton");
Constructor[] constructorsFull = clazzFull.getDeclaredConstructors();
constructorsFull[0].setAccessible(true);
FullSingleton fullReflect = (FullSingleton) constructorsFull[0].newInstance();
System.out.println(fullOrigin == fullReflect);
// 反序列化
String jsonStrFull = JSON.toJSONString(fullOrigin);
FullSingleton fullJson = JSON.parseObject(jsonStrFull, FullSingleton.class);
System.out.println(fullOrigin == fullJson);
/* 测试DCL */
DCL dclOrigin = DCL.getInstance();
// 反射
Class clazzDcl = Class.forName("zhanht.DCL");
Constructor[] constructorsDcl = clazzDcl.getDeclaredConstructors();
constructorsDcl[0].setAccessible(true);
DCL dclReflect = (DCL) constructorsDcl[0].newInstance();
System.out.println(dclOrigin == dclReflect);
// 反序列化
String jsonStrDcl = JSON.toJSONString(dclOrigin);
DCL dclJson = JSON.parseObject(jsonStrDcl, DCL.class);
System.out.println(dclOrigin == dclJson);
/* 测试静态内部类 */
InnerClassSingleton innerOrigin = InnerClassSingleton.getInstance();
// 反射
Class clazzInner = Class.forName("zhanht.InnerClassSingleton");
Constructor[] constructorsInner = clazzInner.getDeclaredConstructors();
constructorsInner[0].setAccessible(true);
InnerClassSingleton innerReflect = (InnerClassSingleton) constructorsInner[0].newInstance();
System.out.println(innerOrigin == innerReflect);
// 反序列化
String jsonStrInner = JSON.toJSONString(dclOrigin);
InnerClassSingleton innerJson = JSON.parseObject(jsonStrInner, InnerClassSingleton.class);
System.out.println(innerOrigin == innerJson);
}
}
代码3:反射和序列化破坏单例
五:终极大法:枚举
那么,是否存在一种实现方式,把反射和序列化也考虑进去了呢?还真有,那就是:枚举。
大家对枚举的使用,一般都是停留在定义各种类型,操作码等。可能也听过枚举实现单例,但可能并没有深究其原因。
接下来,咱们就具体分析分析为什么通过枚举能够完美的实现单例模式。
枚举类型是java语言中的又一块语法糖,用于作为预先定义好的常量的集合。除了它自动继承自Enum类,所以没法继承自枚举类型。除此之外,它和一般的类没太大区别,一样能定义自己的属性和方法。
通过反编译,可以看到,枚举中定义的常量自动是public static final的,构造函数默认是私有的,通过静态代码块调用私有的构造函数对常量进行初始化,因此可以用于实现单例模式,具体大家可以自行通过javap命令或者第三方反编译软件进行查看。
这里大家可能就有疑问了,枚举的实现和前面的方式差不多啊,只不过是隐式的而已。那么枚举通过反射和反序列化后生成的对象还是原来的对象吗?下面通过一个例子来试验下。
public class Test2 {
public static void main(String[] args) throws Exception {
CodeEnum origin = CodeEnum.A;
/* 反射方式 */
Class clazz = Class.forName("zhanht.CodeEnum");
Constructor[] constructors = clazz.getDeclaredConstructors();
constructors[0].setAccessible(true);
CodeEnum reflect = (CodeEnum) constructors[0].newInstance("success", 0);
System.out.println("reflect == origin : " + (reflect == origin));// 运行时异常:Exception in thread "main" java.lang.IllegalArgumentException: Cannot reflectively create enum objects
/* 反序列化方式 */
// fastJson 方式实验
String jsonStr = JSON.toJSONString(origin);
CodeEnum json = JSON.parseObject(jsonStr, CodeEnum.class);
System.out.println("json == origin " + (json == origin)); // true
// ObjectOutputStream 方式实验
ByteArrayOutputStream byteArrayInputStream = new ByteArrayOutputStream();
ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayInputStream);
objectOutputStream.writeObject(origin);
objectOutputStream.close();
ObjectInputStream objectInputStream = new ObjectInputStream(new ByteArrayInputStream(byteArrayInputStream.toByteArray()));
CodeEnum stream = (CodeEnum) objectInputStream.readObject();
objectInputStream.close();
System.out.println("stream == origin " + (stream == origin)); // true
}
}
enum CodeEnum {
A("success", 0);
private String desc;
private int code;
CodeEnum(String desc, int code) {
this.desc = desc;
this.code = code;
}
}
代码4:枚举的反射和反序列化
通过实验结果,我们可以发现,枚举通过对反射的拦截来防止反射的破坏。实现的地方在Constructor类的newInstance中,如果发现类型是Enum,就直接抛异常。
那么,枚举怎么保证反序列化后的对象依然是原来的对象呢?通过debug一步步往里面跟,你会发现,最后反序列化的枚举都是通过:Enum.valueOf(Class enumType, String name) 这个方法返回的,此方法通过枚举的具体类型和name,可以定位到最初定义的那个具体实例,它的关键在于这行代码:
T result = enumType.enumConstantDirectory().get(name);
进入enumConstantDirectory方法的具体实现,代码和解释如下。
Map<String, T> enumConstantDirectory() {
/* 懒加载,第一次调用此方法的时候,会取初始化 enumConstantDirectory这个map
* map的key是枚举实例的name,value是name对应的具体实例 */
if (enumConstantDirectory == null) {
// 这个方法通过反射调用枚举具体类型的 values方法,得到所有的实例
T[] universe = getEnumConstantsShared();
if (universe == null)
throw new IllegalArgumentException(
getName() + " is not an enum type");
Map<String, T> m = new HashMap<>(2 * universe.length);
for (T constant : universe)
m.put(((Enum<?>)constant).name(), constant);
enumConstantDirectory = m;
}
return enumConstantDirectory;
}
代码5:枚举反序列化依然为原对象的原因
六:一把无坚不摧的矛:Unsafe类
通过上面的讲解,大家应该明白了,枚举是实现单例模式的一种简单并且安全的方式,也明白了枚举在反射和发序列化情形下,依然能保持单例的实现原理。
那么,枚举实现的单例就无法破坏了吗?
大多数情况下,是的。但是,我们也能有特殊的方式去破坏它,那就是通过sun.misc.Unsafe类。
Unsafe类能够直接和系统底层进行交互,能够直接操作内存,最常见的就是各大高性能组件中经常使用的CAS操作。
由于Unsafe的高危性,所以Java并不鼓励大家直接使用它,所以它被设计成单例,并且只能通过系统引导类进行加载。
下面简单演示下,通过反射调用unsafe直接在内存上绕开一切限制,直接创建对象。
public class Code6 {
public static void main(String[] args) throws Exception {
CodeEnum origin = CodeEnum.A;
Field f = Unsafe.class.getDeclaredField("theUnsafe");
f.setAccessible(true);
Unsafe unsafe = (Unsafe) f.get(null);
CodeEnum unSafe = (CodeEnum) unsafe.allocateInstance(CodeEnum.class);
System.out.println(origin == unSafe); // false
}
}
代码6:unsafe直接创建对象
但这种方式使用毕竟很少,也不鼓励使用,所以一般不予以考虑。所以,通过枚举实现的单例,一般可以认为是安全的。
七:总结
通过如下几块内容的分析讲解,现在总结如下:
实现方式 | 线程安全 | 防反射和反序列化 |
---|---|---|
普通饿汉式 | 是 | 否 |
普通懒汉式 | 否 | 否 |
加锁或者DCL | 是 | 否 |
静态内部类 | 是 | 否 |
枚举 | 是 | 是 |
以上是关于逐步构建一个“铜墙铁壁”的单例模式的主要内容,如果未能解决你的问题,请参考以下文章