彻底玩转单例模式
文章目录
1 单例模式(Singleton Pattern)简介
1.1 概述
一种创建型模式的体现,这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。
注意:
- 单例类只能有一个实例。
- 单例类必须自己创建自己的唯一实例。
- 单例类必须给所有其他对象提供这一实例。
1.2 介绍
**意图:**保证一个类仅有一个实例,并提供一个访问它的全局访问点。
**主要解决:**一个全局使用的类频繁地创建与销毁。
**何时使用:**当您想控制实例数目,节省系统资源的时候。
**如何解决:**判断系统是否已经有这个单例,如果有则返回,如果没有则创建。
**关键代码:**构造函数是私有的。
应用实例:
- 一个班级只有一个班主任。
- Windows 是多进程多线程的,在操作一个文件的时候,就不可避免地出现多个进程或线程同时操作一个文件的现象,所以所有文件的处理必须通过唯一的实例来进行。
- 一些设备管理器常常设计为单例模式,比如一个电脑有两台打印机,在输出的时候就要处理不能两台打印机打印同一个文件。
使用场景:
- 要求生产唯一序列号。
- WEB 中的计数器,不用每次刷新都在数据库里加一次,用单例先缓存起来。
- 创建的一个对象需要消耗的资源过多,比如 I/O 与数据库的连接等。
其中,根据加载对象的时机,单例模式有两种表现形式:饿汉式(立即加载)和懒汉式(延迟加载)。
2 饿汉式(Eager Loading)
饿汉式
概述:立即加载,在虚拟机启动的时候就会创建。在类创建的同时就已经创建好一个静态的对象供系统使用,以后不再改变。无需关注多线程问题,所以饿汉式的单例对象是线程安全的。
优点:没有加锁,执行效率会提高。
缺点:类加载时就初始化,浪费内存。
代码演示:
package com.singleton;
/**
* 饿汉式
*/
public class MySingletonEagerLoad {
// 1.私有化构造器
private MySingletonEagerLoad(){}
// 2.使用final关键字声明要创建的对象为一个不变的对象,并直接创建对象。浪费内存空间
private static final MySingletonEagerLoad singleton = new MySingletonEagerLoad();
// 3.提供外部获取单例的接口
public static MySingletonEagerLoad getSingletonInstance() {
return singleton;
}
}
3 懒汉式(Lazy Loading)
懒汉式
概述:延迟加载,随用随创建。在调用的时候才创建对象,通常情况下是线程不安全的,因为存在对象为null的判断。所以根据线程安全角度,懒汉式可分为:懒汉式-线程不安全、懒汉式-线程安全。若在创建实例对象时使用synchronized关键字同步代码块,可使得线程安全。但这并不是绝对的线程安全,仅仅只是在java代码逻辑上是线程安全的,实际上在底层对象的创建是通过3段程序指令来完成的,但这3段指令存在重新排序的情况,使得对象的创建过程并不是绝对意义上的线程安全。若对于共享单例使用volatile关键字修饰,则表明禁止底层指令重排序,可使得线程绝对安全。
3.1 懒汉式-线程不安全
在创建实例对象时没有使用synchronized同步代码,所以是线程不安全的,在多线程下会得到多个实例,不支持多线程。严格意义上不算单例。
代码演示:
package com.singleton;
/**
* 懒汉式之线程不安全
*
* 无论是饿汉式还是懒汉式都可通过反射破坏单例模式!!!
* 解决方案:使用枚举类。枚举类的源码是不允许通过反射破坏单例性的。
*/
public class MySingletonLazyLoad {
// 1.私有化构造器
private MySingletonLazyLoad(){}
// 2.声明要创建的单例对象
private static MySingletonLazyLoad singleton;
/**
* 3.提供外部获取单例的接口(线程不安全)
*
* @return
*/
public static MySingletonLazyLoad getSingletonInstanceNotSafe() {
if (singleton == null) {
singleton = new MySingletonLazyLoad();
}
return singleton;
}
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
new Thread(() -> {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(MySingletonLazyLoad.getSingletonInstanceNotSafe());
}).start();
}
}
输出结果(当结果都一致时,尽可能加大循环次数,或者加大睡眠时间,后面的测试也都如此):
com.singleton.MySingletonLazyLoad@4f667964
com.singleton.MySingletonLazyLoad@4bf69a32
com.singleton.MySingletonLazyLoad@4bf69a32
com.singleton.MySingletonLazyLoad@77241ae5
com.singleton.MySingletonLazyLoad@4bf69a32
com.singleton.MySingletonLazyLoad@4bf69a32
com.singleton.MySingletonLazyLoad@4bf69a32
com.singleton.MySingletonLazyLoad@4bf69a32
com.singleton.MySingletonLazyLoad@4bf69a32
com.singleton.MySingletonLazyLoad@4bf69a32
3.2 懒汉式-线程安全
线程安全的体现可分为3种实现方式:
- 使用关键字synchronized同步代码块(对于DCL懒汉式需要深究)
- 使用静态内部类
3.2.1 synchronized同步代码块的方式
多线程并发情况下,性能消耗较大。加锁是一个很耗性能的操作。
根据能耗分为:非DCL模式和DCL模式(Double-Check Lock)
3.2.1.1 非DCL模式
代码演示:
package com.singleton;
/**
* 懒汉式之线程安全
* 非DCL模式
* 无论是饿汉式还是懒汉式都可通过反射破坏单例模式!!!
* 解决方案:使用枚举类。枚举类的源码是不允许通过反射破坏单例性的。
*/
public class MySingletonLazyLoad {
// 1.私有化构造器
private MySingletonLazyLoad(){}
// 2.声明要创建的单例对象
private static MySingletonLazyLoad singleton;
/**
* 3.提供外部获取单例的接口(线程安全,但很耗性能)
*
* @return
*/
public static synchronized MySingletonLazyLoad getSingletonInstanceSafe() {
if (singleton == null) {
singleton = new MySingletonLazyLoad();
}
return singleton;
}
// 多线程测试
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
new Thread(() -> {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(MySingletonLazyLoad.getSingletonInstanceSafe());
}).start();
}
}
}
输出结果
com.singleton.MySingletonLazyLoad@4bf69a32
com.singleton.MySingletonLazyLoad@4bf69a32
com.singleton.MySingletonLazyLoad@4bf69a32
com.singleton.MySingletonLazyLoad@4bf69a32
com.singleton.MySingletonLazyLoad@4bf69a32
com.singleton.MySingletonLazyLoad@4bf69a32
com.singleton.MySingletonLazyLoad@4bf69a32
com.singleton.MySingletonLazyLoad@4bf69a32
com.singleton.MySingletonLazyLoad@4bf69a32
com.singleton.MySingletonLazyLoad@4bf69a32
3.2.1.2 DCL模式(Double-Check Lock)
DCL(Double-Check Lock):即双重校验锁。是两层针对单例是否为null的检查,中间嵌套一个同步锁的结构形式。目的是为了降低由同步所带来的性能消耗。使得只是在创建单例的时候有同步锁带来的性能消耗,多线程下再次获取单例对象的时候就会先被外层null检查拦截而不进入同步锁。
代码演示:
package com.singleton;
/**
* 懒汉式之线程安全
* DCL模式
* 无论是饿汉式还是懒汉式都可通过反射破坏单例模式!!!
* 解决方案:使用枚举类。枚举类的源码是不允许通过反射破坏单例性的。
*/
public class MySingletonLazyLoad {
// 1.私有化构造器
private MySingletonLazyLoad(){}
// 2.声明要创建的单例对象,同时声明为volatile,表明禁止指令重排序
private volatile static MySingletonLazyLoad singleton;
/**
* 3.提供外部获取单例的接口(线程安全,优化同步,降低能耗)
* 采取方案:双重校验锁(Double Check Lock) + volatile
*
* @return
*/
public static MySingletonLazyLoad getSingletonInstanceSafe() {
if (singleton == null) {
synchronized(MySingletonLazyLoad.class) {
if (singleton == null) {
/**
* 对象创建的流程(期望):
* 1、分配内存空间
* 2、调用构造器初始化对象
* 3、对象指向这个内存空间
* 指令重排序:这并不是一个原子性的操作,123的执行过程可能会存在置换。
* 当线程A正在执行13---------------2的过程中创建对象时
* 线程B发现此时对象还是null,而实际上线程A只是在加载过程中并很快创建了该对象,此时线程B的访问就是有问题的。
*/
singleton = new MySingletonLazyLoad(); //对象的创建过程本不是一个原子性的操作,但通过volatile禁止了指令重排序,使得线程是绝对安全的
}
}
}
return singleton;
}
// 多线程测试
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
new Thread(() -> {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(MySingletonLazyLoad.getSingletonInstanceSafe());
}).start();
}
}
}
输出结果:
com.singleton.MySingletonLazyLoad@77241ae5
com.singleton.MySingletonLazyLoad@77241ae5
com.singleton.MySingletonLazyLoad@77241ae5
com.singleton.MySingletonLazyLoad@77241ae5
com.singleton.MySingletonLazyLoad@77241ae5
com.singleton.MySingletonLazyLoad@77241ae5
com.singleton.MySingletonLazyLoad@77241ae5
com.singleton.MySingletonLazyLoad@77241ae5
com.singleton.MySingletonLazyLoad@77241ae5
com.singleton.MySingletonLazyLoad@77241ae5
3.2.2 静态内部类的实现方式
这种方式只适用于静态域的情况,双检锁方式可在实例域需要延迟初始化时使用。
MySingletonLazyLoad类被装载了,singleton不一定被初始化。因为 InnerSingleton类没有被主动使用,只有通过显式调用 getSingletonInstanceSafe方法时,才会显式装载 InnerSingleton类,从而实例化 singleton。
代码演示:
package com.singleton;
/**
* 懒汉式
*
* 无论是饿汉式还是懒汉式都可通过反射破坏单例模式!!!
* 解决方案:使用枚举类。枚举类的源码是不允许通过反射破坏单例性的。
*/
public class MySingletonLazyLoad {
// 1.私有化构造器
private MySingletonLazyLoad(){}
/**
* 3.提供外部获取单例的接口(线程安全,不耗性能)
* 返回静态内部类中的单例对象
* @return
*/
public static MySingletonLazyLoad getSingletonInstanceSafe() {
return InnerSingleton.singleton;
}
/**
* 2.声明一个静态内部类
* 用于立即加载单例对象
*/
public static class InnerSingleton {
// 在静态内部类中声明为立即加载的单例,是线程安全的
private static final MySingletonLazyLoad singleton = new MySingletonLazyLoad();
}
// 多线程测试
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
new Thread(() -> {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(MySingletonLazyLoad.getSingletonInstanceSafe());
}).start();
}
}
}
输出结果:
com.singleton.MySingletonLazyLoad@42b95647
com.singleton.MySingletonLazyLoad@42b95647
com.singleton.MySingletonLazyLoad@42b95647
com.singleton.MySingletonLazyLoad@42b95647
com.singleton.MySingletonLazyLoad@42b95647
com.singleton.MySingletonLazyLoad@42b95647
com.singleton.MySingletonLazyLoad@42b95647
com.singleton.MySingletonLazyLoad@42b95647
com.singleton.MySingletonLazyLoad@42b95647
com.singleton.MySingletonLazyLoad@42b95647
4 单例模式的安全问题
单例模式并不安全,可用java的反射机制或反序列化机制破坏其单例性质
4.1 反射机制下对单例的破坏演示
public static void main(String[] args) throws Exception{
MySingletonLazyLoad instance1 = MySingletonLazyLoad.getSingletonInstanceSafe();
Constructor<MySingletonLazyLoad> declaredConstructor = MySingletonLazyLoad.class.getDeclaredConstructor(null);
declaredConstructor.setAccessible(true);
MySingletonLazyLoad instance2 = declaredConstructor.newInstance();
MySingletonLazyLoad instance3 = declaredConstructor.newInstance();
System.out.println(instance1); //com.singleton.MySingletonLazyLoad@1540e19d
System.out.println(instance2); //com.singleton.MySingletonLazyLoad@677327b6
System.out.println(instance3); //com.singleton.MySingletonLazyLoad@14ae5a5
}
输出结果:
com.singleton.MySingletonLazyLoad@1540e19d
com.singleton.MySingletonLazyLoad@677327b6
com.singleton.MySingletonLazyLoad@14ae5a5
很明显,输出结果都不一致!反射对单例的破坏是很致命的!
4.2 反射破坏单例的解决方案
4.2.1 私有构造器中构造异常(治标)
例如:在私有构造器中添加静态标记位(可加密,不易被外界所发现)
private static boolean flag = false;
private MySingletonLazyLoad(){
synchronized (MySingletonLazyLoad.class) {
if (flag == false) {
flag = true;
} else {
throw new RuntimeException("不要试图使用反射破坏单例结构!");
}
}
}
但是依然存在破解的可能性!该标记位即便加密也有被解密的风险,反射同样可以获取到该值进一步去破解!所以这种方式治标不治本。
4.2.2 使用枚举类 (治本)
实际上,实现单例模式的最佳方法就是枚举Enum。它更简洁,自动支持序列化机制,绝对防止多次实例化。它不仅能避免多线程同步问题(也是立即加载),而且还自动支持序列化机制,防止反序列化重新创建新的对象,绝对防止多次实例化。不能通过 reflection attack 来调用私有构造方法实例化。通过反射调用构造器实例化的时候,底层会判断该类型是否为枚举,若是则抛出异常,从而达到单例的绝对有效。
例如:定义一个枚举类Payment,声明多个单例对象
package com.singleton;
public enum Payment {
CASH("现金支付"),
WECHAT_PAY("微信支付"),
ALIPAY("阿里支付"),
BANK_CARD("银行卡支付"),
CREDIT_CARD("信用卡支付");
private String paymentDesc;
Payment() {}
Payment(String paymentDesc){
this.paymentDesc = paymentDesc;
}
public String getPaymentDesc() {
return paymentDesc;
}
}
使用反射机制打算破坏其单例结构
public static void main(String[] args) throws Exception {
Constructor<Payment> paymentConstructor = Payment.class.getDeclaredConstructor(String.class);
paymentConstructor.setAccessible(true);
Payment payment = paymentConstructor.newInstance();
System.out.println(payment);
}
输出结果:
Exception in thread "main" java.lang.NoSuchMethodException: com.singleton.Payment.<init>(java.lang.String)
at java.lang.Class.getConstructor0(Class.java:3082)
at java.lang.Class.getDeclaredConstructor(Class.java:2178)
at com.singleton.MainTest.main(MainTest.java:2)6)
打印出了异常信息,但是控制台显示的异常很诡异:NoSuchMethodException: com.singleton.Payment.(java.lang.String)。这是不合理的。因为在自定义的枚举类Payment中很明显的声明了私有的带参构造器,而这里却显示异常为NoSuchMethodException:没有找到此构造器。。。
这是因为idea编译器骗了我们!使用第三方正规的反编译器jad进行反编译(这里使用的是图形化jad反编译工具FrontEnd Plus):
// Decompiled Using: FrontEnd Plus v2.03 and the JAD Engine
// Available From: http://www.reflections.ath.cx
// Decompiler options: packimports(3)
// Source File Name: Payment.java
package com.singleton;
public final class Payment extends Enum
{
public static Payment[] values()
{
return (Payment[])$VALUES.clone();
}
public static Payment valueOf(String name)
{
return (Payment)Enum.valueOf(com/singleton/Payment, name);
}
private Payment(String s, int i)
{
super(s, i);
}
private Payment(String s, int i, String paymentDesc)
{
super(s, i);
this.paymentDesc = paymentDesc;
}
public String getPaymentDesc()
{
return paymentDesc;
}
public static final Payment CASH;
public static final Payment WECHAT_PAY;
public static final Payment ALIPAY;
public static final Payment BANK_CARD;
public static final Payment CREDIT_CARD;
private String paymentDesc;
private static final Payment $VALUES[];
static
{
CASH = new Payment("CASH", 0, "\u73B0\u91D1\u652F\u4ED8");
WECHAT_PAY = new Payment("WECHAT_PAY", 1, "\u5FAE\u4FE1\u652F\u4ED8");
ALIPAY = new Payment("ALIPAY", 2, "\u963F\u91CC\u652F\u4ED8");
BANK_CARD = new Payment("BANK_CARD", 3, "\u94F6\u884C\u5361\u652F\u4ED8");
CREDIT_CARD = new Payment("CREDIT_CARD", 4, "\u4FE1\u7528\u5361\u652F\u4ED8");
$VALUES = (new Payment[] {
CASH, WECHAT_PAY, ALIPAY, BANK_CARD, CREDIT_CARD
});
}
}
这里可以很清晰的发现每个单例对象的创建都是调用的
private Payment(String s, int i, String paymentDesc)
{
super(s, i);
this.paymentDesc = paymentDesc;
}
这个构造器,所以在idea中报了找不到初始化参数为java.lang.String的构造器的异常。
于是可以预见的,我们通过反射获取正确参数类型的构造器:
public static void main(String[] args) throws Exception {
Constructor<Payment> paymentConstructor = Payment.class.getDeclaredConstructor(String.class,int.class,String.class);
paymentConstructor.setAccessible(true);
Payment payment = paymentConstructor.newInstance();
System.out.println(payment);
}
输出结果:
Exception in thread "main" java.lang.IllegalArgumentException: Cannot reflectively create enum objects
at java.lang.reflect.Constructor.newInstance(Constructor.java:417)
at com.singleton.MainTest.main(MainTest.java:4)
异常信息为IllegalArgumentException: Cannot reflectively create enum objects。无法通过反射创建枚举类型的实例对象。所以通过枚举构建的单例是无法通过反射来破坏的。这是因为在底层Constructor.java中明确声明了对于枚举类通过反射来创建实例对象会抛出异常!
@CallerSensitive
public T newInstance(Object ... initargs)
throws InstantiationException, IllegalAccessException,
IllegalArgumentException, InvocationTargetException
{
if (!override) {
if (!Reflection.quickCheckMemberAccess(clazz, modifiers)) {
Class<?> caller = Reflection.getCallerClass();
checkAccess(caller, clazz, null, modifiers);
}
}
if ((clazz.getModifiers() & Modifier.ENUM) != 0)
throw new IllegalArgumentException("Cannot reflectively create enum objects");
ConstructorAccessor ca = constructorAccessor; // read volatile
if (ca == null) {
ca = acquireConstructorAccessor();
}
@SuppressWarnings("unchecked")
T inst = (T) ca.newInstance(initargs);
return inst;
}
4.3 小结
通过反射机制可以破坏自定义的单例模式(非枚举类),可以在私有构造器中加入一定的逻辑判断(添加标记位)有效阻止这种破坏,但是无法从根源上进行阻止,这是由于反射机制可以随便更改类中的任何信息定义。要从根源上解决这种问题,必须采用JDK5提供的枚举类Enum。因为在底层就已经避免了反射机制的入侵!
5 总结
**经验之谈:**一般情况下,不建议使用3.1和3. 2 .1.1的懒汉方式,建议使用第 2种饿汉式。只有在要明确实现 lazy loading 效果时,才会使用3.2.2 的登记方式/静态内部类方式。如果涉及到反序列化创建对象时,可以尝试使用4.2.2介绍的枚举方式。如果有其他特殊的需求,可以考虑使用3.2.1.2双检锁方式。