设计模式--如何保证单例不被破坏
什么是单例
一个类中只有一个实例,能够向系统中提供一个唯一实例
使用场景
单例能够保证系统内存中只存在一个对象,当频繁创建于销毁对象时,使用单例能够节省很多资源。
缺点
频繁变化的对象不适合使用单例
滥用单例会导致负面问题,如为了节省资源将数据库连接池对象设置为单例对象,可能会导致共享线程池对象的程序过多导致连接池溢出
三种常用方式
1.双重检锁
public class DCLSingleton {
/**
* 构造私有
*/
private DCLSingleton() {
}
/**
* 提供唯一一个实例对象
*/
private static volatile DCLSingleton dclSingleton = null;
/**
* 提供获取对象的访问入口
* @return
*/
public static DCLSingleton getDclSingleton() {
if (dclSingleton == null) {
synchronized (DCLSingleton.class) {
if (dclSingleton == null) {
dclSingleton = new DCLSingleton();
}
}
}
return dclSingleton;
}
}
单例对象使用volatile原因
即通过反射可以破解DCL单例
@Test
public void testDCLSingleton() throws Exception {
// 获取Class对象
Class<DCLSingleton> slc = DCLSingleton.class;
Constructor<DCLSingleton> slcConstructor = slc.getDeclaredConstructor();
// 忽略权限修饰符
slcConstructor.setAccessible(true);
// 尝试构建第一个对象
DCLSingleton obj01 = slcConstructor.newInstance();
DCLSingleton obj02 = slcConstructor.newInstance();
System.out.println(obj01);
System.out.println(obj02);
System.out.println(obj01==obj02);
}
console
singleton.SingletonObj@96532d6
singleton.SingletonObj@3796751b
false
解决被反射破坏的DCL单例
根据《Effective Java中文版 第2版》描述:
享有特权的客户端可通过AccessObject.setAccessible()方法,通过反射调用私有构造器破坏单例。
如果要抵御这种攻击,可以修改构造器,让它被要求创建对象的时候抛出异常。
- DCL线程安全解决方法
修改构造器,在被要求创建第二个实例时抛出异常
public class SafeDCLSingleton {
/**
* 添加一个标识符flag。注意修饰符要为static,目的是当flag被修改,其他对象立即可见
*/
private static boolean flag = false;
/**
* 假如场景如下:
* 线程A先来的,第一次执行构造函数创建对象时,发现flag=false,构造器将flag改为true,
* 这时线程B过来想尝试执行构造函数创建新对象,发现flag=true,就会直接抛出异常,
* 这样保证了DCL下单例不会被破坏。
*/
private SafeDCLSingleton() {
if (!flag) {
flag = true;
} else {
throw new RuntimeException("使用反射破解反射是没有用的");
}
}
private static volatile SafeDCLSingleton dclSingleton = null;
public static SafeDCLSingleton getDclSingleton() {
if (dclSingleton == null) {
synchronized (SafeDCLSingleton.class) {
if (dclSingleton == null) {
dclSingleton = new SafeDCLSingleton();
}
}
}
return dclSingleton;
}
}
2.静态内部类单例
public class StaticSingleton {
/**
* 构造私有
*/
private StaticSingleton() {
}
/**
* 利用类加载子系统在特性,在类初始化阶段 完成对象创建赋值
*/
private static class Singleton {
private static StaticSingleton staticSingleton = new StaticSingleton();
}
/**
* 提供获取唯一实例公共访问的入口
* @return
*/
public StaticSingleton staticSingleton() {
return Singleton.staticSingleton;
}
}
反射破坏DCL单例
@Test
public void testStaticSingleton()throws Exception {
Class<StaticSingleton> ssc = StaticSingleton.class;
Constructor<StaticSingleton> sscConstructor = ssc.getDeclaredConstructor();
sscConstructor.setAccessible(true);
StaticSingleton staticSingleton01 = sscConstructor.newInstance();
StaticSingleton staticSingleton02 = sscConstructor.newInstance();
System.out.println("staticSingleton01 = " + staticSingleton01);
System.out.println("staticSingleton02 = " + staticSingleton02);
System.out.println(staticSingleton01==staticSingleton02);
}
console
staticSingleton01 = com.abu.statics.StaticSingleton@1b0375b3
staticSingleton02 = com.abu.statics.StaticSingleton@2f7c7260
false
同DLC的解决方案
public class StaticSafeSingleton {
private static boolean flag = false;
/**
* 构造私有
*/
private StaticSafeSingleton() {
if (!flag) {
flag = true;
} else {
throw new RuntimeException("不要使用反射破坏单例,没有用的");
}
}
/**
* 使用JVM特性,在类加载器中 初始化阶段 完成对象的赋值
*/
private static class Singleton {
private static StaticSafeSingleton staticSingleton = new StaticSafeSingleton();
}
/**
* 提供获取唯一实例公共访问入口
*
* @return
*/
public StaticSafeSingleton staticSingleton() {
return Singleton.staticSingleton;
}
}
3.枚举单例
枚举特性:枚举实例的创建默认就是线程安全/单例
/**
* @Author liu kang
* @Date 2021/08/06 10:28
* @Description
*/
public enum SingletonEnum {
SINGLE;
private Resource resource;
SingletonEnum() {
resource = new Resource();
}
public Resource getResource() {
return resource;
}
}
// 资源类
class Resource {
private String name;
}
- 反射尝试破解单例
@Test
public void testEnums() throws Exception {
// 获取Class对象
Class<SingletonEnum> singletonEnumClass = SingletonEnum.class;
// 获取无参构造函数对象
Constructor<SingletonEnum> constructorObj = singletonEnumClass.getDeclaredConstructor();
constructorObj.setAccessible(true);
SingletonEnum single01 = constructorObj.newInstance();
System.out.println(single01);
}
- 抛出异常
java.lang.NoSuchMethodException: singleton.SingletonEnum.<init>()
通过反编译jad工具查看枚举
枚举之所以能够保证线程安全/单例,是由于枚举类明确了构造函数为私有化,同时每个枚举常量是用final修饰。也就是当访问枚举常量时,枚举常量只能被实例化一次且不变。
三种方式比较
单例模式比较 | DCL方式 | 静态内部类 | 枚举方式 |
---|---|---|---|
线程安全 | 是 | 是 | 是 |
是否防反射 | 否 | 否 | 是 |
结论
以上三种方式是针对多线程情况下保证单例不被破坏
1. 双重检锁的方式通过 **修改构造器,在被要求创建第二个实例时抛出异常**的方式保证多线程情况下保证单例
2. 静态内部类同DCL方式
3. 枚举方式 是通过JVM来保证多线程情况下保证单例,天然具有线程安全/单例
文章参考
狂神说之设计模式
《Effective Java中文版 第2版》