单例模式被公认为是设计模式中最简单的一种,用于保证系统中,某个类只有一个实例,运用非常广泛。
单例模式,往简单了说,其实关键就是,控制构造函数的访问权限,然后对外提供统一的访问点。
但其实,写好一个单例模式,并没有大家想的那么简单。下面,通过一步步的迭代优化,从线程安全和防破坏两个维度,逐步的实现一个“铜墙铁壁”般牢固的单例模式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.p