项目
博客
文档
归档
资源链接
关于我
项目
博客
文档
归档
资源链接
关于我
设计模式之—— 单例模式
2020-11-12
·
softbabet博主
·
原创
·
设计模式
·
本文共 830个字,预计阅读需要 3分钟。
保证一个类仅有一个实例,并提高一个全局访问点。创建型。 适用场景:想确保任何情况下都绝对只有一个实例。单服务下网站的计数器,集群情况下使用共享计数器,线程池的设计,数据库的连接池。 优点:在内存里只有一个实例,减少了内存开销;可以避免对资源的多重占用。设置全局访问点,严格控制访问。 缺点:没有接口,扩展困难 重点: **私有构造器,线程安全,延迟加载,序列化和反序列化安全,反射** 1.单例-Double Check双重检查锁的内存机制: ![](http://114.67.107.180//ynblog/upload/1605143165669.png) ![](http://114.67.107.180/ynblog/upload/1605143173964.png) 2.单例-静态内部类 --基于类初始化的延迟加载解决方案 ![](http://114.67.107.180/ynblog/upload/1605143182136.png) 使用技能:反编译,内存原理,多线程Debug 相关设计模式结合: 单例+工厂 ===>一些业务场景中,可以把工厂类设计为单例模式的 单例+享元 ====>一些业务场景中,要管理很多单例对象,通过这两个模式的结合来完成单例对象的获取,这种情况下,享元模式类似于单例对象的一个工厂,只不过这个工厂拿出创建好的对象,不会重新创建。 ### 1.懒汉式单例模式 初始化没有创建,而是做一个**延迟加载**。构造器是私有的,为了不让外部new,并且线程不安全,多线程有问题。 ``` public class LazySingleton { private static LazySingleton lazySingleton = null; public static LazySingleton getInstance() { if (lazySingleton == null) { lazySingleton = new LazySingleton(); } } return lazySingleton; } ``` #### 多线程Debug 1.在idea中打上断点,然后右键,选择Suspend为Thread,通过线程来控制。 2.以debug模式启动项目,找到下面左侧的Frames,此时通过下拉来选择线程的切换和控制。 解决懒汉式在多线程中可能引起的问题: 改进防止多线程不安全的可能(创建多个对象),或者在方法上加锁synchronized,会对内存进行开销,消耗资源 改进方式一:getInstance方法上加同步关键字synchronized。 如果getInstance不是静态方法,那么相当于锁的是在堆内存中生成的对象。 加了static,那么锁的的是这个类。 改进方式二:在getInstance里加:synchronized (LazySingleton.class) {xxx } 改进的方式存在的缺点:同步锁比较消耗资源,里面有加锁解锁的开销,同时synchronized 锁的是这个类,范围比较大,性能有问题。 ### 2.Double Check双重鉴定的单例模式 做延迟初始化来降低单例实例的开销。,这种的兼顾了性能和安全,也是懒加载的。 ``` public class LazyDoubleCheckSingleton { private static LazyDoubleCheckSingleton lazyDoubleCheckSingleton = null; private LazyDoubleCheckSingleton() { } public static LazyDoubleCheckSingleton getInstance() { if (lazyDoubleCheckSingleton == null) { //多线程情况下,可能因为指令重排序,导致这里为非空,但是还没有创建出对象 synchronized (LazyDoubleCheckSingleton.class) { if (lazyDoubleCheckSingleton == null) { lazyDoubleCheckSingleton = new LazyDoubleCheckSingleton(); //上面的new其实执行了下面三步操作,因为指令重排序,可能导致执行顺序为1,3,2,就存在对象不为空,但是还没有初始化对象的情况。 //1.分配内存给这个对象 //2.初始化对象 //3.设置lazyDoubleCheckSingleton 指定刚分配的内存空间 } } } return lazyDoubleCheckSingleton; } } ``` 由于指令重排序问题,这种还是存在多线程问题。 解决方式:使用volatile关键字声明这个getInstance,加volatile关键字,**重排序就会被禁止**,所有的线程都能看到共享内存的最新状态,保证了内存的可见性,使用的缓存移植性协议 ``` private volatile static LazyDoubleCheckSingleton lazyDoubleCheckSingleton = null; ``` ### 3.静态内部类的单例模式 做延迟初始化来降低单例实例的开销。线程之间看不到重排序。是线程安全的。关键在于InnerClass这个类的对象的初始化锁,看哪个线程拿到,哪个线程就去初始化它。 ``` //单例-静态内部类 --基于类初始化的延迟加载解决方案,不关心重排序 public class StaticInnerClassSingleton { private static class InnerClass{ private static StaticInnerClassSingleton staticInnerClassSingleton = new StaticInnerClassSingleton(); } public static StaticInnerClassSingleton getInstance(){ return InnerClass.staticInnerClassSingleton; } private StaticInnerClassSingleton() { //单例模式一定要重写构造器且私有化,否则外界能new到 if(InnerClass.staticInnerClassSingleton !=null){ throw new RuntimeException("单例构造器禁止反射调用"); } } } ``` ### 4.饿汉式单例模式 写法简单,类加载的时候就完成了初始化,避免了线程同步问题。缺点就是没有延迟加载的效果,如果这个类从始至终系统没有使用,就会造成内存的浪费。 ``` public class HungrySingleton{ private final static HungrySingleton hungrySingleton = new HungrySingleton(); private HungrySingleton(){ } public static HungrySingleton getInstance(){ return hungrySingleton; } } ``` 方式二:可以将声明的放到静态代码块中: ``` private static HungrySingleton hungrySingleton; static { hungrySingleton =new HungrySingleton(); } ``` #### 单例设计模式-序列化破坏单例模式原理解析及解决 测试:实例化一个单例对象(这个对象要实现序列化implements Serializable),对象序列化后以对象流的形式输出,然后再以反序列化回来,然后比较这两个对象是否相等.发现是不等的。 ``` HungrySingleton instance = HungrySingleton.getInstance(); ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("singleton_file")); oos.writeObject(instance); File file = new File("singleton_file"); ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file)); HungrySingleton newInstance = (HungrySingleton)ois.readObject(); System.out.println(instance ==newInstance); //false ``` 源码分析:进入ObjectInputStream.readObject()方法中,在此方法中调用了readObject0()方法,在类型判断的时候是对象类型 ``` case TC_OBJECT: return checkResolve(readOrdinaryObject(unshared)); ``` 此时调用readOrdinaryObject()方法,返回的是obj: ``` obj = desc.isInstantiable() ? desc.newInstance() : null; ``` 再查看ObjectStreamClass.isInstantiable()方法,通过注释我们可以看到serializable/externalizable这样的类,在运行时可以被实例化,所有返回true,因此obj就是通过反射方式得到的对象desc.newInstance(); readOrdinaryObject方法接着往下走,调用了desc.hasReadResolveMethod())方法,进入这个方法通过注入可以看到只要是serializable 或者externalizable 的类并且写readResolve方法,就返回true;然后就通过反射获取这个方法的返回值: ``` Object rep = desc.invokeReadResolve(obj); ``` ``` readResolveMethod = getInheritableMethod( cl, "readResolve", null, Object.class); ``` ,如果没有写readResolve方法,就按以前上面返回获取的数据,所以是不同的。 **解决方式:** 在**实例对象中添加readResolve方法**,将初始化的返回: ``` private Object readResolve(){ return hungrySingleton; } ``` #### 单例设计模式-反射攻击解决方案 测试:通过反射修改构造器为可访问的权限,破环唯一访问,通过constructor.newInstance来创建新的对象。 ``` Class objectClass = HungrySingleton.class; Constructor constructor = objectClass.getDeclaredConstructor(); constructor.setAccessible(true); HungrySingleton instance = HungrySingleton.getInstance(); HungrySingleton newInstance = (HungrySingleton)constructor.newInstance(); System.out.println(instance == newInstance); ``` 解决方式:对于**静态内部类或者饿汉式**(也就是在**类加载的时候就初始化的类**)的单例模式,**构造器中处理**: ``` private HungrySingleton(){ //这种的也适用于对类加载的时候就把这个对象创建好了的单例模式有效,以及静态类 if(hungrySingleton !=null){ throw new RuntimeException("单例构造器禁止反射调用"); } } ``` 对于**懒汉式或者双重鉴定的模式中**,如果反射获取在前,getInstance方法调用在后,那么是无效的,反之有效。不能保证先后顺序,所以**反射是无法避免**的。 如果定义计数flag或者先后量,然后通过此数据来做业务处理,其实是没有意义的,因为返回照样能拿到此数据做修改。 #### 单例设计模式-Enum枚举单例、原理源码解析以及反编译实战 枚举类型的单例模式的序列化模式,反射攻击。枚举类天然的可以序列化机制能够强有力的保证不会出现多次实例化的情况,即使在复杂的序列化或者反射的攻击性下,枚举类型的单例模式都没有问题。**枚举类型的单例是实现单例模式的最佳实践**。 ``` /** * 枚举类型的单例模式(最佳选择),可以避免序列化(天然可序列化)和反射攻击,不会出现多次实例化的情况 * */ public enum EnumInstance { INSTANCE; private Object data; public Object getData() { return data; } public void setData(Object data) { this.data = data; } public static EnumInstance getInstance(){ return INSTANCE; } } ``` 按照前面测试序列化和反射攻击的方式,结果都是返回true,因此枚举不受这些破环。 按照上面序列化分析源码: 进入ObjectInputStream.readObject()方法中,在此方法中调用了readObject0()方法,在类型判断的时候是对象类型 ``` case TC_ENUM: return checkResolve(readEnum(unshared)); ``` 此时调用readEnum()方法,进入方法可以看到: ``` String name = readString(false); ``` 通过readString获取枚举对象的名称,再通过这个name获取枚举常量,因为枚举常量的name唯一,对于一个常量。所以拿到的是唯一常量,维持了单一属性。 ``` Enum> en = Enum.valueOf((Class)cl, name); ``` 分析枚举反射源码: 进入Enum抽象类中,定义了唯一有参构造器: ``` protected Enum(String name, int ordinal) { this.name = name; this.ordinal = ordinal; } ``` 因此,反射是不能创建无参的构造器的,再在Constructor类中的newInstance方法中定义了不能通过反射创建枚举的构造器: ``` if ((clazz.getModifiers() & Modifier.ENUM) != 0) throw new IllegalArgumentException("Cannot reflectively create enum objects"); } ``` 通过反编译工具将枚举类编译成class文件,通过反编辑工具看生成的类的特点: **枚举类编译后是一个final类,同时是有一个私有有参构造器,同时类变量是静态常量,没有延迟初始化,通过静态代码块在类加载的时候把这个对象就初始化完成。所以它是线程安全的**。 枚举类的扩展使用:将INSTANCE修改,外部可以调用:EnumInstance.getInstance().printTest() ``` INSTANCE{ protected void printTest(){ System.out.println("printTest"); } }; protected abstract void printTest(); ``` #### 单例设计模式-容器(注册式)单例 容器的单例模式,可以管理多个对象,通过map来实现单例对象容器 优点:当项目中单例应用叫多时(有很需要多个单例的对象),可以使用容器的单例模式,放到容器中统一管理 缺点:它是线程不安全的,在多个线程存通过同一个key的时候可能是有不同的对象实例,前面的实例会被最后创建的实例覆盖到map中。多线程中存在隐患。 如果使用HashTable可以是线程安全的,但是在取的时候会频繁的使用锁,会影响性能,不建议使用。 ``` public class ContainerSingleton { private static Map
singletonMap = new HashMap<>(); private ContainerSingleton() { } public static void putInstance(String key, Object instance){ if(StringUtils.isNoneBlank(key) && instance != null){ if(!singletonMap.containsKey(key)){ singletonMap.put(key,instance); } } } public static Object getInstance(String key){ return singletonMap.get(key); } } ``` ### 单例设计模式-ThreadLocal线程单例 TreadLocal线程"单例",不能保证整个应用全局唯一,但是可以整个线程唯一 它是基于ThreadLocalInstance带引号的单例 ``` class ThreadLocalInstance { private static final ThreadLocal
ThreadLocalInstanceTreadLocal = new ThreadLocal
(){ @Override protected ThreadLocalInstance initialValue() { return new ThreadLocalInstance(); } }; private ThreadLocalInstance() { } public static ThreadLocalInstance getInstance(){ return ThreadLocalInstanceTreadLocal.get(); } } ``` ### 单例模式源码应用(jdk+spring+mybatis) JDK: Runtime.getRuntime() ==>饿汉式 Desktop.getDesktop() ===>synchronized同步方法,AppContext.getAppContext(); --容器单例模式 Spriing: AbstractFactoryBean.getObject() Mybatis: ErrorContext.instance() ===>hreadLocal
--线程单例模式 ServletContext、ServletConfig、ApplicationContext,DBPool