设计模式 - 单例模式

实例

数据库连接池

  • 假设一个数据库连接池的创建场景,将指定个数的数据库连接对象存储在连接池中,客户端可以从池中随机取一个连接对象来连接数据库,设计一个能够自行提供指定个数实例对象的数据库连接类

  • 数据库连接池是系统开发需要面对和考虑的问题,主要是减少重复连接数据库的代价;在系统中创建预期数量的数据库连接,并将这些连接以一个集合或类似生活中的池一样管理起来,用到的时候直接拿过来使用,用完返回给系统管理;为了减少系统资源开销,提高创建速度,以及全局共享对象,可以通过单例模式来实现


单例模式

概念

  • 单例模式(Singleton Pattern),确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例,这个类称为单例类,它提供全局访问的方法
  • 单例模式是一种对象创建型模式
  • 单例模式结构图(来源于刘伟老师技术博客)
    在这里插入图片描述

懒汉式单例(LazySingleton)

  • DBConnectionProvider.java
/**
 * @Description 数据库连接池(懒汉式单例)
 */
public class DBConnectionProvider {

    private static DBConnectionProvider dbConnProvider = null;

    /**
     * 数据库连接对象集合
     */
    private static LinkedList<Object> dbList = null;

    /**
     * 私有构造函数
     */
    private DBConnectionProvider() {}

    /**
     * 返回唯一实例
     * @return
     */
    public static DBConnectionProvider getInstance() {
        if (dbConnProvider == null) {
            dbConnProvider = new DBConnectionProvider();
        }

        return dbConnProvider;
    }

    /**
     * 定义数据库连接池大小
     * @param size
     */
    public void defineSizeOfConnProvider(int size) {
        dbList = new LinkedList<>();
        for (int i = 0; i < size; i++) {
            dbList.add(new Object());
        }
    }

    /**
     * 随机获取数据库连接对象
     * @return
     */
    public Object getConnProviderMember() {
        Random random = new Random();
        return dbList.get(random.nextInt(dbList.size()));
    }

}
  • Test.java
/**
 * @Description 懒汉式单例测试类
 * 下述例子均使用该测试类
 */
public class Test {
    public static void main(String[] args) {
        DBConnectionProvider dbConnectionProvider = DBConnectionProvider.getInstance();
        DBConnectionProvider dbConnectionProvider1 = DBConnectionProvider.getInstance();
        DBConnectionProvider dbConnectionProvider2 = DBConnectionProvider.getInstance();

        if (dbConnectionProvider == dbConnectionProvider1 && dbConnectionProvider1 == dbConnectionProvider2) {
            System.out.println("获取到唯一实例");
        }

        dbConnectionProvider.defineSizeOfConnProvider(5);

        for (int i = 0; i < 5; i++) {
            System.out.println("获取连接池对象:" + dbConnectionProvider.getConnProviderMember());
        }

    }
}
  • 输出如下:
获取到唯一实例
获取连接池对象:java.lang.Object@677327b6
获取连接池对象:java.lang.Object@677327b6
获取连接池对象:java.lang.Object@14ae5a5
获取连接池对象:java.lang.Object@7f31245a
获取连接池对象:java.lang.Object@677327b6
  • 如上代码,忽略不调用defineSizeOfConnProvider()直接调用getConnProviderMember()的异常
  • 懒汉式单例在第一次调用getInstance()方法时实例化,在类加载时并不自行实例化,这种技术又称为延迟加载(Lazy Load)技术,即需要的时候再加载实例
  • 懒汉式单例并不是线程安全的,javanew过程实际经历了3个步骤
1.分配内存给该对象
2.初始化对象
3.设置instance指向刚分配的内存地址
  • 在实际的new过程中23的步骤可能重排序(java允许通过这样提高性能),这在多线程情况下存在2、3步骤重排序后线程1访问到线程0还未初始化的对象,此时new代码将再次执行,最后创建多个instance对象
  • 该场景存在如下解决方法:
1.懒汉式单例模式DoubleCheck双重检查:通过声明volatile关键字不允许2、3重排序
2.饿汉式单例模式:通过类加载时初始化instance静态变量,确保单例对象的唯一性
3.静态内部类单例模式:允许2、3重排序,但不允许其它线程看到这个重排序
  • 缺点:懒汉式单例存在线程安全问题

懒汉式单例DoubleCheck双重检查(LazyDoubleCheck)

  • DBConnectionProvider.java
/**
 * @Description 数据库连接池(懒汉式单例DoubleCheck双重检查)
 */
public class DBConnectionProvider {

    private static DBConnectionProvider dbConnProvider = null;

    /**
     * 数据库连接对象集合
     */
    private static LinkedList<Object> dbList = null;

	/**
     * 私有构造函数
     */
    private DBConnectionProvider() {}

    /**
     * 返回唯一实例
     * @return
     */
    public static DBConnectionProvider getInstance() {
        // TODO 第一重判断
        if (dbConnProvider == null) {
            // TODO 锁定代码块
            synchronized (DBConnectionProvider.class) {
                // TODO 第二重判断
                if (dbConnProvider == null) {
                    // TODO 创建单例实例
                    dbConnProvider = new DBConnectionProvider();
                }
            }
        }

        return dbConnProvider;
    }

    /**
     * 定义数据库连接池大小
     * @param size
     */
    public void defineSizeOfConnProvider(int size) {
        dbList = new LinkedList<>();
        for (int i = 0; i < size; i++) {
            dbList.add(new Object());
        }
    }

    /**
     * 随机获取数据库连接对象
     * @return
     */
    public Object getConnProviderMember() {
        Random random = new Random();
        return dbList.get(random.nextInt(dbList.size()));
    }

}
  • synchronized同步锁使方法变成同步方法,synchronized可以加在方法上,但synchronized修饰static静态方法时锁的是class文件,范围较广,对性能有一定影响,即每次调用getInstance()时都需要进行线程锁定判断,在多线程高并发访问环境中,将会导致系统性能大大降低,synchronized修饰方法伪代码如下:
public synchronized static LazySingleton getInstance() {
    if (lazySingleton == null) {
        lazySingleton = new LazySingleton();
    }

    return lazySingleton;
}
  • volatile关键字声明共享变量,所有的线程都能看到共享内存的执行状态,volatile关键字修饰的共享变量在进行写操作时会将当前处理器缓存好的数据写回到系统内存,该操作会使在其它CPU里缓存了该内存地址的数据无效,再次从共享内存同步数据(缓存一致性协议),通过这样保证内存的可见性
  • 缺点:懒汉式单例DoubleCheck双重检查由于使用了volatile关键字(会屏蔽Java虚拟机所做的一些代码优化),可能会导致系统运行效率降低

饿汉式单例(HungrySingleton)

  • DBConnectionProvider.java
/**
 * @Description 数据库连接池(饿汉式单例)
 */
public class DBConnectionProvider {

    private final static DBConnectionProvider dbConnProvider;

	/**
     * 初始化静态变量
     */
    static {
        dbConnProvider = new DBConnectionProvider();
    }

    /**
     * 私有构造函数
     */
    private DBConnectionProvider() {}

    /**
     * 数据库连接对象集合
     */
    private static LinkedList<Object> dbList;

    /**
     * 返回唯一实例
     * @return
     */
    public static DBConnectionProvider getInstance() {
        return dbConnProvider;
    }

    /**
     * 定义数据库连接池大小
     * @param size
     */
    public void defineSizeOfConnProvider(int size) {
        dbList = new LinkedList<>();
        for (int i = 0; i < size; i++) {
            dbList.add(new Object());
        }
    }

    /**
     * 随机获取数据库连接对象
     * @return
     */
    public Object getConnProviderMember() {
        Random random = new Random();
        return dbList.get(random.nextInt(dbList.size()));
    }

}
  • 当类被加载时,静态变量dbConnProvider会被初始化,此时类的私有构造函数会被调用,单例类的唯一实例将被创建。使用饿汉式单例来实现数据库连接池DBConnectionProvider类的设计,则不会出现创建多个单例对象的情况,可确保单例对象的唯一性
  • 缺点:饿汉式单例不能实现延迟加载,不论有没有被使用,其始终占用内存

静态内部类单例(StaticInnerClassSingleton)

  • DBConnectionProvider.java
/**
 * @Description 数据库连接池(静态内部类单例模式)
 */
public class DBConnectionProvider {

    private static LinkedList<Object> dbList;

     /**
      * 静态内部类
      * 静态内部类单例模式的核心在于InnerClass对象的初始化锁
      */
    private static class InnerClass {
        private static final DBConnectionProvider dbConnectionProvider = new DBConnectionProvider();
    }

    /**
     * 私有构造函数
     */
    private DBConnectionProvider() {}

    /**
     * 返回唯一实例
     * @return
     */
    public static DBConnectionProvider getInstance() {
        return InnerClass.dbConnectionProvider;
    }

    /**
     * 定义数据库连接池大小
     * @param size
     */
    public void defineSizeOfConnProvider(int size) {
        dbList = new LinkedList<>();
        for (int i = 0; i < size; i++) {
            dbList.add(new Object());
        }
    }

    /**
     * 随机获取数据库连接对象
     * @return
     */
    public Object getConnProviderMember() {
        Random random = new Random();
        return dbList.get(random.nextInt(dbList.size()));
    }

}
  • 静态内部类单例模式是基于类初始化的延迟加载解决方案,它可以解决多线程下重排序问题(静态内部类单例模式允许重排序,但不允许其它线程看到这个重排序),其原理实:JVM在类的初始化阶段(class被加载后且被线程使用之前都是类的初始化阶段)会执行类的初始化,在执行类的初始化期间,JVM会获取一个锁(Class对象初始化锁),这个锁可以同步多个线程对一个类的初始化,基于该特性,可以实现基于静态内部类的线程安全的延迟初始化方案(非构造线程不允许看到重排序)
  • 静态内部类和DoubleCheck都是为了做延迟初始化来降低创建单例实例的开销
  • 饿汉式单例与懒汉式单例都存在相应的缺点,而静态内部类单例能够将这两种单例的缺点都客服,将其优点合二为一

Enum枚举单例(EnumInstance)

  • DBConnectionProvider.java
/**
 * @Description 数据库连接池(Enum枚举单例)
 */
public enum DBConnectionProvider {
    INSTANCE {
        /**
         * 定义数据库连接池大小
         * @param size
         */
        protected void defineSizeOfConnProvider(int size) {
            dbList = new LinkedList<>();
            for (int i = 0; i < size; i++) {
                dbList.add(new Object());
            }
        }

        /**
         * 随机获取数据库连接对象
         * @return
         */
        protected Object getConnProviderMember() {
            Random random = new Random();
            return dbList.get(random.nextInt(dbList.size()));
        }
    };

    /**
     * 数据库连接对象集合
     */
    private static LinkedList<Object> dbList;

    protected abstract void defineSizeOfConnProvider(int size);

    protected abstract Object getConnProviderMember();

    /**
     * 返回唯一实例
     * @return
     */
    public static DBConnectionProvider getInstance() {
        return INSTANCE;
    }

}
  • enumjdk1.5引入的语法糖,它不是java中的新增类型,编译器在编译阶段会自动将它转换成一个继承于Enum的子类
  • INSTANCE最后会被编译器处理成static final的,并且在static模块中进行的初始化,因此它的实例化是在class被加载阶段完成,是线程安全的

ThreadLocal单例(ThreadLocalInstance)

  • DBConnectionProvider.java
public class DBConnectionProvider {

    private final static ThreadLocal<DBConnectionProvider> threadLocal = ThreadLocal.withInitial(DBConnectionProvider::new);

    /**
     * 数据库连接对象集合
     */
    private static LinkedList<Object> dbList;

    /**
     * 私有构造函数
     */
    private DBConnectionProvider() {}

    /**
     * 返回唯一实例
     * @return
     */
    public static DBConnectionProvider getInstance() {
        return threadLocal.get();
    }

    /**
     * 定义数据库连接池大小
     * @param size
     */
    public void defineSizeOfConnProvider(int size) {
        dbList = new LinkedList<>();
        for (int i = 0; i < size; i++) {
            dbList.add(new Object());
        }
    }

    /**
     * 随机获取数据库连接对象
     * @return
     */
    public Object getConnProviderMember() {
        Random random = new Random();
        return dbList.get(random.nextInt(dbList.size()));
    }

}
  • 基于ThreadLocal的特性,ThreadLocal"单例" 不能保证全局唯一,但是可以线程唯一,每个线程中拿到的实例都是一个,不同的线程拿到的实例不是一个,应称为ThreadLocal线程单例

容器单例(ContainerSingleton)

  • 容器单例是非线程安全的,适合程序初始化时放入多个单例对象统一管理,这里只记录一个简单案例
  • ContainerSingleton.java
/**
 * @Description 容器单例(非线程安全)
 */
public class ContainerSingleton {

    private ContainerSingleton() {}

    private static Map<String, Object> singletonMap = new HashMap<String, Object>();

    public static void putInstance(String key, Object instance) {
        if(Objects.nonNull(key) && !key.isEmpty() && Objects.nonNull(instance)){
            if (!singletonMap.containsKey(key)) {
                singletonMap.put(key, instance);
            }
        }

    }

    public static Object getInstance(String key) {
        return singletonMap.get(key);
    }
}
  • Test.java
/**
 * @Description 容器单例测试类(存在线程安全问题)
 */
public class Test {
    public static void main(String[] args) {
        ContainerSingleton.putInstance("object", new Object());
        Object instance = ContainerSingleton.getInstance("object");
    }
}

总结

模式 线程安全 调用效率 延时加载 作用域
懒汉式单例 DoubleCheck 不高 可以 全局唯一
饿汉式单例 安全 不可以 全局唯一
静态内部类单例 安全 可以 全局唯一
Enum枚举单例 安全 不可以 全局唯一
ThreadLocal枚举单例 安全 不可以 线程唯一
ThreadLocal枚举单例 安全 不可以 线程唯一

扩展

序列化破坏单例模式

  • 序列化攻击代码如下:
  • DBConnectionProvider.java
// 继承序列化接口
public class DBConnectionProvider implements Serializable {
	...
}
  • Test.java
public class Test {
    public static void main(String[] args) throws Exception {

        /**
         * 序列化攻击测试,代码适用于除枚举单例的以上举例的所有单例模式
         */
        DBConnectionProvider instance = DBConnectionProvider.getInstance();

        ObjectOutputStream oss = new ObjectOutputStream(new FileOutputStream("singleton_file"));
        oss.writeObject(instance);

        File file = new File("singleton_file");
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file));
        DBConnectionProvider newInstance = (DBConnectionProvider) ois.readObject();

        System.out.println(instance);
        System.out.println(newInstance);
        System.out.println(instance == newInstance);
    }
}
  • 输出如下:
com.coisini.design.pattern.creational.singleton.own.lazysingleton.DBConnectionProvider@135fbaa4
com.coisini.design.pattern.creational.singleton.own.lazysingleton.DBConnectionProvider@58372a00
false
  • 可以看出,序列化后写入写出的对象不是同一个,解决方案是在单例类中添加readResolve()方法
  • DBConnectionProvider.java
/**
 * 固定名称,ObjectStreamClass 521行源码中 固定该名称
 * @return
 */
public Object readResolve() {
    return dbConnProvider;
}
  • 输出如下:
com.coisini.design.pattern.creational.singleton.own.lazysingleton.DBConnectionProvider@135fbaa4
com.coisini.design.pattern.creational.singleton.own.lazysingleton.DBConnectionProvider@135fbaa4
true
  • 此处序列化攻击不包含枚举类,枚举类天然的序列化机制能够强有力的保证不会出现多次实例化的情况
  • 枚举单例序列化破坏代码测试输出如下:
INSTANCE
INSTANCE
true

反射攻击单例模式

反射攻击饿汉式

  • 反射攻击代码如下:
  • Test.java
public class Test {
    public static void main(String[] args) throws Exception {
        /**
         * 反射攻击测试代码
         */
        Class objClass = DBConnectionProvider.class;
        Constructor constructor = objClass.getDeclaredConstructor();
        constructor.setAccessible(true);

        DBConnectionProvider instance = DBConnectionProvider.getInstance();
        DBConnectionProvider newInstance = (DBConnectionProvider) constructor.newInstance();

        System.out.println(instance);
        System.out.println(newInstance);
        System.out.println(instance==newInstance);
    }
}
  • 输出如下:
// 反射攻击后获取的对象不是同一个
com.coisini.design.pattern.creational.singleton.own.hungrysingleton.DBConnectionProvider@1540e19d
com.coisini.design.pattern.creational.singleton.own.hungrysingleton.DBConnectionProvider@677327b6
false
  • 饿汉式单例对于反射攻击的解决方案如下:
  • DBConnectionProvider.java
/**
 * 私有构造函数
 */
private DBConnectionProvider() {
	// 原因是饿汉式在类加载时就完成初始化
    if (dbConnProvider != null) {
        throw new RuntimeException("单例构造器禁止反射调用");
    }
}
  • 输入如下:

在这里插入图片描述

  • 结论:饿汉模式可以防住反射攻击

反射攻击懒汉式

  • 懒汉式单例对于反射攻击的解决方案如下:
  • DBConnectionProvider.java
private static boolean flag = true;

/**
 * 私有构造函数
 */
private DBConnectionProvider() {
    /**
     * 添加逻辑判断
     */
    if (flag) {
        flag = false;
    } else {
        throw new RuntimeException("单例构造器禁止反射调用");
    }
}
  • 输出结果如下:

在这里插入图片描述

  • 但这种防御机制依然可以通过反射修改逻辑判断的关键属性,代码如下:
    Test.java
public class Test {
    public static void main(String[] args) throws Exception {
        /**
         * 反射攻击测试
         */
        Class objClass = DBConnectionProvider.class;
        Constructor constructor = objClass.getDeclaredConstructor();
        constructor.setAccessible(true);

        DBConnectionProvider instance = DBConnectionProvider.getInstance();
        // DBConnectionProvider newInstance = (DBConnectionProvider) constructor.newInstance();

        // TODO 修改逻辑判断属性值反射攻击
        Field flag = instance.getClass().getDeclaredField("flag");
        // TODO 修改权限
        flag.setAccessible(true);
        // TODO 修改属性
        flag.set(instance, true);
        DBConnectionProvider newInstance = (DBConnectionProvider) constructor.newInstance();

        System.out.println(instance);
        System.out.println(newInstance);
        System.out.println(instance==newInstance);
    }
}
  • 输出如下:
// 通过修改关键逻辑属性值又造成了反射攻击
com.coisini.design.pattern.creational.singleton.own.lazysingleton.DBConnectionProvider@14ae5a5
com.coisini.design.pattern.creational.singleton.own.lazysingleton.DBConnectionProvider@7f31245a
false
  • 结论:对于懒汉模式,防御不住反射攻击

反射攻击枚举单例

  • 反射攻击枚举单例测试代码如下:
public class Test {
   public static void main(String[] args) throws Exception {
       /**
        * 反射攻击枚举单例测试
        */
       Class classObj = DBConnectionProvider.class;
       Constructor constructor = classObj.getDeclaredConstructor(String.class, int.class);
       constructor.setAccessible(true);
       DBConnectionProvider enumInstance = (DBConnectionProvider) constructor.newInstance("Hello", 1);

       DBConnectionProvider newEnumInstance = DBConnectionProvider.getInstance();
       System.out.println(enumInstance);
       System.out.println(newEnumInstance);
       System.out.println(enumInstance==newEnumInstance);
   }
}
  • 输出结果如下:

在这里插入图片描述

  • Java中,enum被限制只能声明private的构造方法来防止Enum被使用new进行实例化,而且还限制了使用反射的方法不能通过ConstructornewInstance一个枚举实例。在尝试使用反射得到的Constructor来调用其newInstance方法来实例化enum时,会得到一个exception

  • 结论:枚举单例能防御反射攻击

序列化破坏与反射攻击总结

模式 线程安全 调用效率 延时加载 作用域 序列化破坏 反射攻击
懒汉式单例 DoubleCheck 不高 可以 全局唯一 可以防御 不可以防御
饿汉式单例 安全 不可以 全局唯一 可以防御 可以防御
静态内部类单例 安全 可以 全局唯一 可以防御 可以防御
Enum枚举单例 安全 不可以 全局唯一 可以防御 可以防御
ThreadLocal枚举单例 安全 不可以 线程唯一 可以防御 我也木知呀

总结

  • 优点
1.在内存中只有一个实例,减少了内存开销
2.可以避免对资源的多重占用
3.设置全局访问点,严格控制访问
  • 缺点
1.没有接口,扩展困难
  • 适用场景
1.想确保任何情况下都绝对只有一个实例
  • Java中应用单例模式的案例
Runtime、Desktoop、AbstractFactoryBean(Spring)、ErrorContext(Mybatis)

源码


- End -
- 个人学习笔记 -
- 仅供参考 -

posted @ 2022-03-15 23:15  Maggieq8324  阅读(105)  评论(0编辑  收藏  举报