博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
单例设计模式
阅读量:3963 次
发布时间:2019-05-24

本文共 8830 字,大约阅读时间需要 29 分钟。

单例设计模式

一、主要解决的问题场景

避免⼀个全局使⽤的类频繁的创建和消费,提升整体的代码性能,减少内存开支

二、主要实现方式

2.1 饿汉式

public class HungryMan {
/** * 饿汉式,类加载的时候就实例化对象 */ private static final HungryMan HUNGRY_MAN = new HungryMan(); /** * 构造方法私有化,外部无法访问并通过空参构造创建新的对象 */ private HungryMan() {
} /** * 对外只提供一个获取对象的方法,每次调用只返回同一个对象 */ public static HungryMan getInstance() {
return HUNGRY_MAN; }}//测试@Testpublic void testHungry() throws InterruptedException {
for (int i = 0; i < 2; i++) {
new Thread(() -> {
HungryMan instance = HungryMan.getInstance(); System.out.println(Thread.currentThread().getName() + "------" + System.identityHashCode(instance)); //我们使用System.identityHashCode获取对象的hash值,检查是否为同一个对象 }).start(); } Thread.currentThread().join();}//我们可以多线程调用,返回的都是同一个对象Thread-1------374756906Thread-2------374756906Thread-0------374756906Thread-3------374756906Thread-4------374756906

2.2 懒汉式

(1)非线程安全

public class LazyMan {
private static LazyMan lazyMan; private LazyMan() {
} public static LazyMan getInstance() {
if (null == lazyMan) {
//检查是否有多个线程重新创建对象,导致并发问题 System.out.println("线程" + Thread.currentThread().getName() + ", 重新创建了对象"); lazyMan = new LazyMan(); } return lazyMan; }}//我们多线程调用,会发现Thread-1、Thread-3、线程Thread-4、Thread-2都重新创建对象,且其hashcode不一致,返回的不是同一个对象线程Thread-1, 重新创建了对象线程Thread-3, 重新创建了对象Thread-3------1362804950线程Thread-4, 重新创建了对象Thread-4------1017499014线程Thread-2, 重新创建了对象Thread-2------1824846967Thread-1------374756906Thread-0------1824846967

对比饿汉式,主要有以下三点区别

a. 饿汉式声明变量同时初始化对象,懒汉式调用方法时初始化对象

b. 在第一次调用对象前,懒汉式比饿汉式节省空间,饿汉式在类加载的时候就实例化,生命周期长

c. 由于饿汉式是类加载实例化对象,所以不存在线程安全问题

(2)DCL双重锁检验懒汉式

public static LazyMan getInstance() {
if (null == lazyMan) {
synchronized (LazyMan.class) {
if (null == lazyMan) {
System.out.println("线程" + Thread.currentThread().getName() + ", 重新创建了对象"); lazyMan = new LazyMan(); } } } return lazyMan;}

我们也可以在getInstance()方法上加synchronized,但是这样会大大降低执行效率,本来多个线程执行这个方法,大多数都是可以直接在第一个if就直接return掉,但在方法上加锁后,就需要集体等待锁释放。

第二层的判断主要是防止,当AB两个线程都在第一层判为空,A拿到锁执行实例化对象,B在A释放锁后也执行,就会出现并发问题。

对于这种DCL模式,还有一个问题就是:指令重排序

创建对象的过程一般是如下顺序:

(1)堆中开辟空间
(2)调用构造方法初始化
(3)把地址赋值给栈中变量
但是JVM会考虑到效率问题,出现无序写入现象:赋值语句在对象实例化之前调用,从而使顺序变为(1)、(3)、(2),可能会出现A线程执行到(3),但还未初始化属性,此时,B线程开始执行,经过第一层的if判断,lazyMan != null,直接返回了属性未初始化的lazyMan 的情况。

//增加volatile,解决指令重排序private static volatile LazyMan lazyMan;

2.3 静态内部类

public class StaticInnerSingle {
//外部类加载时并不需要立即加载内部类,内部类不被加载则不去初始化INSTANCE,故而不占内存 private static class Holder {
private static final StaticInnerSingle STATIC_INNER_SINGLE = new StaticInnerSingle(); } //调用getInstance方法的时候,会加载内部类 public static StaticInnerSingle getInstance() {
return Holder.STATIC_INNER_SINGLE; }}

静态内部类保证单例的原因:类初始化阶段,JVM保证同一个类的static{}方法只被执行一次,JVM靠类的全限定类名以及加载它的类加载器来唯一确定一个类,并保证是同一个类。

2.4 CAS算法单例

public class CASSingle {
//AtomicReference类提供了一个可以原子读写的对象引用变量 private static final AtomicReference
INSTANCE = new AtomicReference<>(); private static CASSingle casSingle; private CASSingle() {
} public static CASSingle getInstance() {
for (;;) {
CASSingle casSingle = INSTANCE.get(); if (null != casSingle) {
return casSingle; } //比较&交换操作,1、获取预期值null;2、实例化新对象;3、获取内存值比较,一致,则引用 if(INSTANCE.compareAndSet(null, new CASSingle())) {
return INSTANCE.get(); } } }}

CAS单例是原子操作,意味着尝试更改相同AtomicReference的多个线程,不会使AtomicReference最终达到不一致的状态。

(1)不需要使⽤传统的加锁⽅式保证线程安全,⽽是依赖于CAS的忙等算法,依赖于底层硬件的实现,来保证线程安全。相对于其他锁的实现没有线程的切换和阻塞也就没有了额外的开销,并且可以⽀持较⼤的并发性

(2)缺点就是忙等,一直没有获取到就会死循环;另外就是会创建大量的CASSingle对象

2.5 枚举单例

public enum EnumSingle {
INSTANCE; EnumSingle() {
} public static EnumSingle getInstance() {
return INSTANCE; }}

枚举单例是线程安全的,但是效率相对低。

三、反射破解

3.1 反射破解方式

以懒汉式为例,进行单例反射破解

@Testpublic void testReflectSingle() throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
//获取私有构造方法,获取访问权限,创建对象 Constructor
declaredConstructor = LazyMan.class.getDeclaredConstructor(); declaredConstructor.setAccessible(true); LazyMan lazyMan = LazyMan.getInstance(); LazyMan lazyManReflect = declaredConstructor.newInstance(null); System.out.println("lazyMan = " + System.identityHashCode(lazyMan)); System.out.println("lazyManReflect = " + System.identityHashCode(lazyManReflect));}//打印结果lazyMan = 366004251lazyManReflect = 1791868405

除了枚举,其他的单例模式实现方式都是可以被破解的,主要原因在于空参的构造方法可以反射获取到,因此我们可以使用如下的解决办法——对空参构造方法进行判断处理。

/** * 空参构造方法,防止反射破解处理 */private LazyMan() {
synchronized (LazyMan.class) {
if (null != lazyMan) {
throw new RuntimeException("禁止反射破解!!"); } }}//输出结果Caused by: java.lang.RuntimeException: 禁止反射破解!! at com.jd.domain.single.HungryMan.
(HungryMan.java:21) ... 27 more

但是,如果我们从一开始没有使用getInstance()实例化对象,直接反射获取对象,那么依然无法阻止反射破解。

//直接反射创建对象LazyMan lazyManReflect1 = declaredConstructor.newInstance(null);LazyMan lazyManReflect2 = declaredConstructor.newInstance(null);//打印lazyManReflect1 = 366004251lazyManReflect2 = 1791868405

主要是因为直接反射创建对象的时候,没有操作成员变量lazyman的实例化,每次判断都是空,都能创建成功。

我们可以设置一个私有成员变量,第一次通过空参构造实例化对象的时候,修改掉这个变量值,如若再次通过反射实例化,可以利用这个变量进行判定。

private static boolean baaccfedaceddfa = false;private LazyMan() {
synchronized (LazyMan.class) {
if (baaccfedaceddfa) {
throw new RuntimeException("禁止反射破解!!"); } baaccfedaceddfa = true; }}//打印结果Caused by: java.lang.RuntimeException: 禁止反射破解!! at com.jd.domain.single.LazyMan.
(LazyMan.java:19) ... 27 more

即使如此,如果我们可以获得这个变量的名称,以入可以获得访问控制,修改为原始状态,同样反射破解成功

//获取到这个成员变量名,获得访问权限,将值修改为false即可再次破解Field baaccfedaceddfa = LazyMan.class.getDeclaredField("baaccfedaceddfa");baaccfedaceddfa.setAccessible(true);baaccfedaceddfa.setBoolean(LazyMan.class, false);LazyMan lazyManReflect2 = declaredConstructor.newInstance(null);//打印结果lazyManReflect1 = 1791868405lazyManReflect2 = 1260134048

3.2 枚举禁止反射破解

我们再枚举单例里定义了一个空参构造方法

public enum EnumSingle {
INSTANCE; //空参构造 EnumSingle() {
} public static EnumSingle getInstance() {
return INSTANCE; }}

然后我们使用反射获取这个空参构造,进行实例化对象

Constructor
declaredConstructor = EnumSingle.class.getDeclaredConstructor();declaredConstructor.setAccessible(true);EnumSingle enumSingleReflect = declaredConstructor.newInstance(null);//打印结果java.lang.NoSuchMethodException: com.jd.domain.single.EnumSingle.
()

出现异常,主要原因是这个枚举类并没有无参构造,这就有点黑人问好了???!!!

我们使用XJad对这个class文件进行反编译,看一下java是否在编译过程中进行了什么神操作。

//final修饰的类,不能被继承public final class EnumSingle extends Enum {
public static final EnumSingle INSTANCE; ...... //替换为有参构造 private EnumSingle(String s, int i) {
super(s, i); } public static EnumSingle getInstance() {
return INSTANCE; } //静态代码块,类加载时就实例化对象 static {
//有参构造内容 INSTANCE = new EnumSingle("INSTANCE", 0); $VALUES = (new EnumSingle[] {
INSTANCE }); }}

可以发现,枚举类,其实就是在编译的时候继承了一个Enum基类,也确实取消了无参构造,而实际使用的是参构造。

既然找到了有参构造的内容,即INSTANCE 和 0,那么我们可以通过有参构造方式反射破解。

Constructor
declaredConstructor = EnumSingle.class.getDeclaredConstructor(String.class, int.class);declaredConstructor.setAccessible(true);EnumSingle enumSingleReflect = declaredConstructor.newInstance("INSTANCE", 0);//打印结果:禁止反射创建枚举对象java.lang.IllegalArgumentException: Cannot reflectively create enum objects

在JDK的反射包里,newInstance方法中,就有对枚举的断言

public T newInstance(Object ... initargs)    throws InstantiationException, IllegalAccessException,           IllegalArgumentException, InvocationTargetException {
...... /*如果是枚举类型,禁止反射破解*/ if ((clazz.getModifiers() & Modifier.ENUM) != 0) throw new IllegalArgumentException("Cannot reflectively create enum objects"); ......}

四、克隆“破解单例”

以饿汉式为例子,需要遵从序列化接口Serializable,然后我们使用Hutool的深度克隆进行序列化操作。

HungryMan instance = HungryMan.getInstance();HungryMan cloneInstance = ObjectUtil.cloneByStream(instance);//打印结果14845944891758386724

很明显,不能满足单例要求。

但其实,这已经与我们使用单例的目的背道而驰了,我们使用单例,是为了保证全局唯一,而我们使用克隆,就是不想全局唯一,互不干扰。

我们点开Enum枚举类的JDK源码,会发现,枚举是天然支持禁止序列化和反序列化的

/** * prevent default deserialization */private void readObject(ObjectInputStream in) throws IOException,    ClassNotFoundException {
throw new InvalidObjectException("can't deserialize enum");}private void readObjectNoData() throws ObjectStreamException {
throw new InvalidObjectException("can't deserialize enum");}

总结枚举单例模式更简洁,⽆偿地提供了串⾏化机制,绝对防⽌对此实例化,即使是在⾯对复杂的串⾏化或者反射攻击的时候。虽然这中⽅法还没有⼴泛采⽤,但是单元素的枚举类型已经成为实现Singleton的最佳⽅法,但是在继承情景下不适用

五、Spring中的单例模式应用

转载地址:http://xtgzi.baihongyu.com/

你可能感兴趣的文章
格式化输出
查看>>
重定向输入输出
查看>>
数据类型之哈希
查看>>
正则表达式
查看>>
线程池
查看>>
包和模块
查看>>
类和对象
查看>>
分叉/结合池
查看>>
日期和时间
查看>>
文件与目标操作
查看>>
Java 原子操作
查看>>
调用操作系统命令
查看>>
JavaMail 精萃
查看>>
Quartz
查看>>
JExcelApi
查看>>
JDBC 精萃
查看>>
比较字符串
查看>>
Java EE 精萃
查看>>
Open Source 精萃
查看>>
Java EE 简介
查看>>