设计模式--如何保证单例不被破坏

什么是单例

一个类中只有一个实例,能够向系统中提供一个唯一实例

使用场景

单例能够保证系统内存中只存在一个对象,当频繁创建于销毁对象时,使用单例能够节省很多资源。

缺点

  1. 频繁变化的对象不适合使用单例

  2. 滥用单例会导致负面问题,如为了节省资源将数据库连接池对象设置为单例对象,可能会导致共享线程池对象的程序过多导致连接池溢出

三种常用方式

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原因

image

即通过反射可以破解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工具查看枚举

image

枚举之所以能够保证线程安全/单例,是由于枚举类明确了构造函数为私有化,同时每个枚举常量是用final修饰。也就是当访问枚举常量时,枚举常量只能被实例化一次且不变

三种方式比较

单例模式比较 DCL方式 静态内部类 枚举方式
线程安全
是否防反射

结论

以上三种方式是针对多线程情况下保证单例不被破坏

1. 双重检锁的方式通过 **修改构造器,在被要求创建第二个实例时抛出异常**的方式保证多线程情况下保证单例

2. 静态内部类同DCL方式

3. 枚举方式 是通过JVM来保证多线程情况下保证单例,天然具有线程安全/单例

文章参考

设计模式——实现优雅的单例模式

狂神说之设计模式

《Effective Java中文版 第2版》

posted @ 2021-08-07 15:56  永无八哥  阅读(159)  评论(0编辑  收藏  举报