设计模式(1):单例模式及其几种实现方式 您所在的位置:网站首页 设计模式及其应用场景怎么写 设计模式(1):单例模式及其几种实现方式

设计模式(1):单例模式及其几种实现方式

2024-06-29 20:34| 来源: 网络整理| 查看: 265

单例模式

1、单例模式简述

2、特点介绍

3、单例模式的几种实现方式

3.1 懒汉模式

3.2 饿汉模式

3.3、静态内部类实现模式

3.4 前述三个方法实现存在的问题

3.5、枚举模式实现

3.6 Java JDK中单例模式的应用

1、单例模式简述

单例模式(Singleton Pattern)是 Java 中最简单的设计模式之一。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。

注意:

1、单例类只能有一个实例。2、单例类必须自己创建自己的唯一实例。3、单例类必须给所有其他对象提供这一实例。 2、特点介绍

(1)意图:保证一个类仅有一个实例,并提供一个访问它的全局访问点。

(2)主要解决:一个全局使用的类频繁地创建与销毁。

(3)何时使用:当您想控制实例数目,节省系统资源的时候。

(4)如何解决:判断系统是否已经有这个单例,如果有则返回,如果没有则创建。

(5)关键代码:构造函数是私有的。

优点:

1、在内存里只有一个实例,减少了内存的开销,尤其是频繁的创建和销毁实例(比如管理学院首页页面缓存)。2、避免对资源的多重占用(比如写文件操作)。

缺点:没有接口,不能继承,与单一职责原则冲突,一个类应该只关心内部逻辑,而不关心外面怎么样来实例化。

使用场景:

1、要求生产唯一序列号。2、WEB 中的计数器,不用每次刷新都在数据库里加一次,用单例先缓存起来。3、创建的一个对象需要消耗的资源过多,比如 I/O 与数据库的连接等。 3、单例模式的几种实现方式 3.1 懒汉模式

特点:

延时加载,只有在真正使用的时候,才开始实例化。

关键点:

(1)线程安全问题;多线程下出现重复创建问题。 (2)doble check 加锁优化(防止多线程情况下重复创建) (3)编译器(JIT),CPU有可能对指令进行重排序,导致使用到尚未初始化的实例,可以通过添加volatile关键字进行修饰,对应volatile修饰的字段,可以防止指令重排。

代码实现

public class LazySingletonTest { public static void main(String[] args) { // LazySingleton instance = LazySingleton .getInstance(); // LazySingleton instance1 = LazySingleton.getInstance(); // System.out.println(instance==instance1); new Thread(()->{ LazySingleton instance = LazySingleton.getInstance(); System.out.println(instance); }).start(); new Thread(()->{ LazySingleton instance = LazySingleton.getInstance(); System.out.println(instance); }).start(); } } /** * 1、单例模式定义: * 保证一个类只有一个实例,并且提供一个全局访问点。 * * 2、场景: * 重量级的对象,不需要多个实例,如线程池,数据库连接池。 */ /** * 3、单例模式的经典实现: * 3.1、懒汉模式:延时加载,只有在真正使用的时候,才开始实例化。 * (1)线程安全问题;多线程下出现重复创建问题。 * (2)doble check 加锁优化(防止多线程情况下重复创建) * (3)编译器(JIT),CPU有可能对指令进行重排序,导致使用到尚未初始化的实例,可以通过添加volatile关键字进行修饰, * 对应volatile修饰的字段,可以防止指令重排。 */ class LazySingleton{ private volatile static LazySingleton instance; private LazySingleton(){ } public static LazySingleton getInstance(){ if(instance==null){ //双重检测机制 synchronized(LazySingleton.class){ //加锁保障并发环境下的线程安全 if(instance==null){ //双重检测机制 instance = new LazySingleton(); //字节码层面 //JIT,CPU指令重排 //1.分配空间 //2.初始化 //3.引用赋值 //注意:由于编译器的指令重排,可能使得其不是安全的,在此将instance设置为volatile类型可防止指令重排,保证其安全性。 } } } return instance; } } 3.2 饿汉模式

特点:

类加载的初始化阶段就完成了实例的初始化。本质上就是借助jvm类加载机制,保证实例的唯一性;

关键点:

(1)类加载的过程:     ①加载二进制数据到内存中,生成对应的class数据结构,     ②连接:验证,准备(给类的静态成员变量赋默认值),解析     ③初始化:给类的静态变量赋初值。 (2)只有在真正使用对应的类是,才会触发初始化

代码实现:

public class HungrySingletonTest { public static void main(String[] args) { HungrySingleton instance = HungrySingleton.getInstance(); HungrySingleton instance1=HungrySingleton.getInstance(); System.out.println(instance==instance1); } } /** * 3.2、饿汉模式:类加载的初始化阶段就完成了实例的初始化。本质上就是借助jvm类加载机制,保证实例的唯一性; * (1)类加载的过程: * ①加载二进制数据到内存中,生成对应的class数据结构, * ②连接:验证,准备(给类的静态成员变量赋默认值),解析 * ③初始化:给类的静态变量赋初值。 * (2)只有在真正使用对应的类是,才会触发初始化 */ class HungrySingleton{ private static HungrySingleton instance = new HungrySingleton(); private HungrySingleton(){ } public static HungrySingleton getInstance(){ return instance; } } 3.3、静态内部类实现模式

特点:

使用静态内部类来实现。

关键点:

(1)本质上是利用类的加载机制来保证线程安全; (2)只有在实际使用的时候,才会触发类的初始化,所以也是懒加载的一种形式;

代码实现:

public class InnerClassSingletonTest { public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException { // InnerClassSingleton instance = InnerClassSingleton.getInstance(); // InnerClassSingleton instance1 = InnerClassSingleton.getInstance(); // System.out.println(instance==instance1); /*new Thread(()->{ InnerClassSingleton instance2 = InnerClassSingleton.getInstance(); System.out.println(instance2); }).start(); new Thread(()->{ InnerClassSingleton instance2 = InnerClassSingleton.getInstance(); System.out.println(instance2); }).start();*/ //测试反射实例化存在的问题: Constructor declaredConstructor = InnerClassSingleton.class.getDeclaredConstructor(); declaredConstructor.setAccessible(true); InnerClassSingleton innerClassSingleton = declaredConstructor.newInstance(); InnerClassSingleton instance = InnerClassSingleton.getInstance(); System.out.println(innerClassSingleton==instance); } } /** * 3.3、静态内部类模式: * (1)本质上是利用类的加载机制来保证线程安全; * (2)只有在实际使用的时候,才会触发类的初始化,所以也是懒加载的一种形式; */ class InnerClassSingleton{ private static class InnerClassHolder{ private static InnerClassSingleton instance = new InnerClassSingleton(); } private InnerClassSingleton(){ } public static InnerClassSingleton getInstance(){ return InnerClassHolder.instance; } } 3.4 前述三个方法实现存在的问题

前述的单例模式的三种实现方式:懒汉模式、饿汉模式、静态内部类实现模式,都会存在反射攻击的问题。

(1)由于Java反射机制的存在,反射可以绕过单例模式创建的类实例,再次创建类实例对象,破坏了单例模式。 (2)解决方法,可以在类的私有构造函数里面进行检查判断,不允许创建多个实例。(懒汉模式无法进行防护)

反射攻击示例如下:(以静态内部类实现示例)

//测试主要代码如下: //测试反射实例化存在的问题: Constructor declaredConstructor = InnerClassSingleton.class.getDeclaredConstructor(); declaredConstructor.setAccessible(true); InnerClassSingleton innerClassSingleton = declaredConstructor.newInstance(); InnerClassSingleton instance = InnerClassSingleton.getInstance(); System.out.println(innerClassSingleton==instance); //测试的结果为: //false

上次示例说明,通过反射会破坏单例模式。对于饿汉模式和静态内部类模式,我们可以在代码中添加代码判断以防止反射创建实例攻击。通过在私有构造函数中添加判断即可,以静态内部类为例:

private InnerClassSingleton(){ if(InnerClassHolder.instance!=null){ throw new RuntimeException("单例不允许创建多个实例"); } }

然而,对于懒汉模式,由于实例时在第一次使用时才在获取实例的函数代码创建的,因为上述方法并不能防止反射攻击。

对于单例模式,我们有更好的实现模式——枚举类实现方式。

3.5、枚举模式实现

特点:

(1)天然不支持反射创建对应的实例,且有字节的反序列化机制;

(2)利用类加载机制保证线程安全。

代码实现:

public enum EnumSingleton{ INSTANCE; public void print(){ System.out.println(this.hashCode()); } } class EnumSingletonTest { public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException, IOException, ClassNotFoundException { /*EnumSingleton instance=EnumSingleton.INSTANCE; EnumSingleton instance1 = EnumSingleton.INSTANCE; System.out.println(instance==instance1);*/ //反射测试 /*Constructor declaredConstructor = EnumSingleton.class.getDeclaredConstructor(String.class, int.class); declaredConstructor.setAccessible(true); //执行报错,Enum类型不允许通过反射创建 EnumSingleton instance=declaredConstructor.newInstance("INSTANCE",0);*/ } }

原理分析:

我们打开JDK源码中java.lang.reflect包,对反射构造类进行源码分析,对于newInstance方法,如下:

public T newInstance(Object ... initargs) throws InstantiationException, IllegalAccessException, IllegalArgumentException, InvocationTargetException { if (!override) { Class caller = Reflection.getCallerClass(); checkAccess(caller, clazz, clazz, modifiers); } if ((clazz.getModifiers() & Modifier.ENUM) != 0) //重点在此,反射机制不允许创建多个单例对象 throw new IllegalArgumentException("Cannot reflectively create enum objects"); ConstructorAccessor ca = constructorAccessor; // read volatile if (ca == null) { ca = acquireConstructorAccessor(); } @SuppressWarnings("unchecked") T inst = (T) ca.newInstance(initargs); return inst; }

上述源码说明,在Java中,反射机制不允许创建多个单例对象实例,因而使用枚举类创建实例具体天然的反反射攻击,可以保护单例模式对象的唯一性。

此外,对于对象实例的序列化,对于普通模式创建的单例,是需要实现java.io.serializable接口并指定版本号的(不指定的话,由JVM默认生成,但是代码一经修改就会改变版本号,就会出现对象反序列化不一致的情况)。

3.6 Java JDK中单例模式的应用

JDK中很多地方应用了单例模式,下面为几个示例,详情可以查看源码具体内容:

(1)Runtime类:典型的饿汉模式 (2)Spring中DefaultSingletonBeanRegistry提供了注册单例容器,添加单例到容器,获取单例的方法。 (3)Spring中的ReactiveAdapterRegister类使用了volatile防止指令重排,也使用了懒汉模式实现单例。 (4)Spring中的proxyFactoryBean类,使用代理模式来创建单例类。

本文代码链接:https://github.com/JianfuYang/2020-yjf-review/tree/master/src/designpatterns/singleton

声明:上述部分内容整理自网络,仅作为本人平时学习记录。

如有侵权,请联系删除!

 



【本文地址】

公司简介

联系我们

今日新闻

    推荐新闻

    专题文章
      CopyRight 2018-2019 实验室设备网 版权所有