栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > Java

设计模式 - 单例模式

Java 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

设计模式 - 单例模式

目录

实例

数据库连接池 单例模式

概念懒汉式单例(LazySingleton)懒汉式单例DoubleCheck双重检查(LazyDoubleCheck)饿汉式单例(HungrySingleton)静态内部类单例(StaticInnerClassSingleton)Enum枚举单例(EnumInstance)ThreadLocal单例(ThreadLocalInstance)容器单例(ContainerSingleton)总结 扩展

序列化破坏单例模式反射攻击单例模式

反射攻击饿汉式反射攻击懒汉式反射攻击枚举单例 序列化破坏与反射攻击总结 总结源码

实例 数据库连接池

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

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


单例模式 概念

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

懒汉式单例(LazySingleton)

DBConnectionProvider.java

public class DBConnectionProvider {

    private static DBConnectionProvider dbConnProvider = null;

    
    private static linkedList dbList = null;

    
    private DBConnectionProvider() {}

    
    public static DBConnectionProvider getInstance() {
        if (dbConnProvider == null) {
            dbConnProvider = new DBConnectionProvider();
        }

        return dbConnProvider;
    }

    
    public void defineSizeOfConnProvider(int size) {
        dbList = new linkedList<>();
        for (int i = 0; i < size; i++) {
            dbList.add(new Object());
        }
    }

    
    public Object getConnProviderMember() {
        Random random = new Random();
        return dbList.get(random.nextInt(dbList.size()));
    }

}
 

Test.java

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)技术,即需要的时候再加载实例懒汉式单例并不是线程安全的,java中new过程实际经历了3个步骤

1.分配内存给该对象
2.初始化对象
3.设置instance指向刚分配的内存地址

在实际的new过程中2和3的步骤可能重排序(java允许通过这样提高性能),这在多线程情况下存在2、3步骤重排序后线程1访问到线程0还未初始化的对象,此时new代码将再次执行,最后创建多个instance对象该场景存在如下解决方法:

1.懒汉式单例模式DoubleCheck双重检查:通过声明volatile关键字不允许2、3重排序
2.饿汉式单例模式:通过类加载时初始化instance静态变量,确保单例对象的唯一性
3.静态内部类单例模式:允许2、3重排序,但不允许其它线程看到这个重排序

缺点:懒汉式单例存在线程安全问题
懒汉式单例DoubleCheck双重检查(LazyDoubleCheck)

DBConnectionProvider.java

public class DBConnectionProvider {

    private static DBConnectionProvider dbConnProvider = null;

    
    private static linkedList dbList = null;

	
    private DBConnectionProvider() {}

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

        return dbConnProvider;
    }

    
    public void defineSizeOfConnProvider(int size) {
        dbList = new linkedList<>();
        for (int i = 0; i < size; i++) {
            dbList.add(new Object());
        }
    }

    
    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

public class DBConnectionProvider {

    private final static DBConnectionProvider dbConnProvider;

	
    static {
        dbConnProvider = new DBConnectionProvider();
    }

    
    private DBConnectionProvider() {}

    
    private static linkedList dbList;

    
    public static DBConnectionProvider getInstance() {
        return dbConnProvider;
    }

    
    public void defineSizeOfConnProvider(int size) {
        dbList = new linkedList<>();
        for (int i = 0; i < size; i++) {
            dbList.add(new Object());
        }
    }

    
    public Object getConnProviderMember() {
        Random random = new Random();
        return dbList.get(random.nextInt(dbList.size()));
    }

}
 

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

DBConnectionProvider.java

public class DBConnectionProvider {

    private static linkedList dbList;

     
    private static class InnerClass {
        private static final DBConnectionProvider dbConnectionProvider = new DBConnectionProvider();
    }

    
    private DBConnectionProvider() {}

    
    public static DBConnectionProvider getInstance() {
        return InnerClass.dbConnectionProvider;
    }

    
    public void defineSizeOfConnProvider(int size) {
        dbList = new linkedList<>();
        for (int i = 0; i < size; i++) {
            dbList.add(new Object());
        }
    }

    
    public Object getConnProviderMember() {
        Random random = new Random();
        return dbList.get(random.nextInt(dbList.size()));
    }

}
 

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

DBConnectionProvider.java

public enum DBConnectionProvider {
    INSTANCE {
        
        protected void defineSizeOfConnProvider(int size) {
            dbList = new linkedList<>();
            for (int i = 0; i < size; i++) {
                dbList.add(new Object());
            }
        }

        
        protected Object getConnProviderMember() {
            Random random = new Random();
            return dbList.get(random.nextInt(dbList.size()));
        }
    };

    
    private static linkedList dbList;

    protected abstract void defineSizeOfConnProvider(int size);

    protected abstract Object getConnProviderMember();

    
    public static DBConnectionProvider getInstance() {
        return INSTANCE;
    }

}
 

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

DBConnectionProvider.java

public class DBConnectionProvider {

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

    
    private static linkedList dbList;

    
    private DBConnectionProvider() {}

    
    public static DBConnectionProvider getInstance() {
        return threadLocal.get();
    }

    
    public void defineSizeOfConnProvider(int size) {
        dbList = new linkedList<>();
        for (int i = 0; i < size; i++) {
            dbList.add(new Object());
        }
    }

    
    public Object getConnProviderMember() {
        Random random = new Random();
        return dbList.get(random.nextInt(dbList.size()));
    }

}
 

基于ThreadLocal的特性,ThreadLocal“单例” 不能保证全局唯一,但是可以线程唯一,每个线程中拿到的实例都是一个,不同的线程拿到的实例不是一个,应称为ThreadLocal线程单例
容器单例(ContainerSingleton)

容器单例是非线程安全的,适合程序初始化时放入多个单例对象统一管理,这里只记录一个简单案例ContainerSingleton.java

public class ContainerSingleton {

    private ContainerSingleton() {}

    private static Map singletonMap = new HashMap();

    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

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

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进行实例化,而且还限制了使用反射的方法不能通过Constructor来newInstance一个枚举实例。在尝试使用反射得到的Constructor来调用其newInstance方法来实例化enum时,会得到一个exception

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

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

总结

优点

1.在内存中只有一个实例,减少了内存开销
2.可以避免对资源的多重占用
3.设置全局访问点,严格控制访问

缺点

1.没有接口,扩展困难

适用场景

1.想确保任何情况下都绝对只有一个实例

Java中应用单例模式的案例

Runtime、Desktoop、AbstractFactoryBean(Spring)、ErrorContext(Mybatis)

源码

GitHub:https://github.com/Maggieq8324/design_pattern.git

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

转载请注明:文章转载自 www.mshxw.com
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号