1. 单例模式的核心:两私一公
1. 本单例类的private的static属性
private static Object instance,用于接收本单例类的单例实例。
2. 本单例类的private的构造方法
防止别的类通过new 的方式创建。
(但是可以被反射破解)
3. 本单例类的public的static方法getInstance
因为别的类不能拿到对象,所以只能通过类名点方法触发创建对象。即Public静态方法。
2. 饿汉模式
(类加载时构造实例,耗时)(线程安全)
为什么叫饿汉:因为是Hungry 类初始化时创建单例,而不是需要时再创建单例。
public class Hungry { private static Hungry instance = new Hungry(); private Hungry() { } public static Hungry getInstance() { return instance; } }
3. 普通懒汉模式
(N个线程同时调用getInstance,会忽略if (instance == null) ,会生成N个Lazy类的实例,即N个单例实例,线程不安全)(单线程场景没毛病)
为什么叫懒汉:因为是需要时再创建单例,而不是类加载时创建单例。
public class Lazy { private static Lazy instance; private Lazy (){ } public static Lazy getInstance() { if (instance == null) { instance = new Lazy(); } return instance; } }
4. 普通懒汉模式——>Synchronized 懒汉模式
(虽然线程安全,但是在高并发的情况下,所有线程都必须执行synchronized方法,串行性能下降)
public class SyncLazy { private static SyncLazy instance; private SyncLazy() { } public static synchronized SyncLazy getInstance() { if (instance == null) { instance = new SyncLazy(); } return instance; } }
5. 普通懒汉模式——>Synchronized 懒汉模式——> Volatile + DCL( Double Check Lock)
此模式在Synchronized 懒汉模式的基础上:
1. synchronized方法改为方法内部的synchronized块 + synchronized块外部加了一个判空的逻辑
2. singleton属性被volatile修饰
改动1的原因:
避免了所有线程都执行synchronized方法,提高效率。而是改为只有第一次创建单例时且有并发竞争时的两个线程才会执行synchronized方法
改动2的原因:
而Volatile的作用有两个:
1. 可见性:线程A实例化属性之后,Volatile刷入主存
2. 防止指令重排:
singleton = new Singleton();的正常顺序是:
1. 声明属性时,为对象分配堆空间,初始时为null(3行)
2. 堆中初始化对象(11行的后半句),为Store指令。
3. 栈中的指针指向堆的地址空间(11行的后=前半句),为Load指令。
但是没有Volatile修饰singleton 属性时,
2和3会发生指令重排,如果3排到了线程A退出monitorexit+B线程monitorenter+B线程判空的之后,则B线程判空结果为true,则B线程又会创建一个单例实例,所以此时堆中就会有两个实例。
所以,为了防止指令Store-Load重排,使用Volatile修饰singleton 属性
public class Singleton { private volatile static Singleton singleton; private Singleton(){} public static Singleton getInstance(){ if(singleton == null){ synchronized (Singleton.class){ if(singleton == null){ singleton = new Singleton(); } } } return singleton; } }
6. 普通懒汉模式——>静态内部类模式
静态内部类的静态属性的初始化(即创建目标单例实例)时机:访问静态内部类的静态属性时(即调用Singleton类 点 getInstance()方法时)
静态内部类天然防止多线程问题。
为什么是懒汉模式而不是饿汉模式:
因为饿汉模式实在类加载时就已经创建完单例了,而懒汉模式则是需要时(即访问静态内部类的静态属性时)再创建。
public class Singleton { private Singleton(){ } public static Singleton getInstance(){ return SingletonHolder.Instance; } private static class SingletonHolder { private static final Singleton Instance = new Singleton(); } }
7. FAQ
7.4 如何防止反射攻击DCL:
以上的单例模式实现虽然很好,但是如果使用反射为私有构造函数setAccessible,然后通过私有构造函数.newInstance()的话,就会把单例模式破解。因为是触发了第二次new对象。
我们可以稍作修改防止这种反射攻击:
把构造函数改成:
private Singleton(){ if (instance!= null) { throw new RunTimeException(“单例模式不允许创建多个实例”) } } // instance代表的是单例类里面的存放单例的那个属性。