Java设计模式精讲—慕课网—课程笔记3
8 单例模式讲解+Coding+源码解析
8.1 单例模式讲解
- 定义:保证一个类仅有一个实例,并提供一个全局访问点;
类型:创建型; - 适用场景:想确保任何情况下都绝对只有一个实例;
- 优点:
在内存里只有一个实例,减少了内存开销;
可以避免对资源的多重占用;
设置全局访问点,严格控制访问; - 缺点:没有接口,扩展困难;
- 重点:私有构造器,线程安全,延迟加载,序列化和反序列化安全,反射;
- 实用技能:反编译,内存原理,多线程debug;
- 相关设计模式:
单例模式和工厂模式:可将工厂类设计为单例模式;
单例模式和享元模式:结合完成单例对象的获取,类似工厂拿出已创建好的对象;
8.2 单例设计模式——懒汉式及多线程Debug实战
- 懒汉模式:初始化时未创建(null),构造函数为private;
延迟加载:只有使用时才初始化; - 多线程调用:
Thread t1 = new Thread(new T());
Thread t2 = new Thread(new T());
t1.start;
t2.start; - 多线程debug:
断点 -> 右键 -> Thread,控制线程执行顺序(make default:之后打的断点采用此模式);
在下方Frames中切换线程; - synchronized方法:(消耗较大)
锁静态方法:锁整个class;
锁非静态方法:锁堆中的对象;
8.3 单例设计模式——DoubleCheck双重检查实战及原理解析
- 加锁保证只有一个线程能创建对象;
- 实际new有3个步骤:
1) 分配内存给这个对象;
2) 初始化对象;
3) 指向内存地址;(2、3可能被重排序)
Java语言规范:intra-thread semantics重排序不会改变单线程内的执行结果;
对象未完全初始化完成,线程1可能就进行访问,发生报错;
3. 方法1:禁止2、3重排序,private volatile static,CPU共享内存;
volatile:所有线程都能看到共享内存状态,保证内存可见性;
变量写操作时会将当前处理器缓存行数据写回内存,使其他CPU里缓存的数据无效,重新从内存中读取数据;
8.4 单例设计模式——静态内部类——基于类初始化的延迟加载解决方案及原理解析
- Java中5种情况下类会立刻被初始化:
有一个A类型实例被创建;
A类中声明的静态方法被调用;
A类中声明的静态成员被赋值;
A类中声明的静态成员被使用,且不是常量成员;
A类如果是一个顶级类,且A中有嵌套的断言语句;
8.5 单例设计模式——饿汉式
- 优点:类加载时完成实例化,避免线程同步问题;写法简单;
缺点:没有延迟加载效果,如果类未被使用,造成内存浪费;
8.6 单例设计模式——序列化破坏单例模式原理解析及解决方案
- 直接序列化、反序列化后,拿到了不同对象;
- 在类中添加方法后返回同一对象:
否则在readOrdinaryObject()中,desc.isInstantiable()返回true,会创建一个新的对象;
定义了readResolve()方法后,ObjectStreamClass中的hasReadResolveMethod()返回true;
过程中其实已经实例化了新对象,但返回的是原有对象。 - debug源码:单步执行进入框架和源码学习。
8.7 单例设计模式——反射攻击解决方案及原理分析
1. 对饿汉式,直接反射获得构造器,失败
通过反射将权限设置为true,成功
2. 关于反射防御的代码
在私有构造器中判断
(适用于类加载时刻就将对象创建好了的方法:饿汉式、静态内部类)
3. 对于不在类加载时创建对象的方法:
如果仍在构造器中添加上述代码:是否破坏单例与构造顺序有关;(多线程不安全)
添加静态计数量等,在构造器中判断计数;(不安全,反射能够获得计数器权限)
测试:(成功禁止调用)
添加修改flag的代码:
flag方法失效。
这类方式如懒汉式、双重检查等无法避免该问题。
8.8 单例设计模式—Enum枚举单例、原理源码解析以及反编译实战
- 枚举类天然可序列化机制,能够强有力保证不会出现多次实例化;(单例模式最佳实现之一,effective java推荐)
- 枚举类序列化:ObjectInputStream -> readEnum()
- 枚举类反序列化:enum只有有参构造器,且不能反射构造枚举对象;
- 反编译工具JAD:https://varaneckas.com/jad/
8.9 单例设计模式——容器单例
- 在程序初始化时,将多个单例对象放入singletonMap统一管理,在使用时通过key直接获取单例对象;
- 多线程:初始化放置对象时可能放多个,放置完成后取时只能取到一个(同一key值后加的Object会覆盖);
- HashMap本身线程不安全,容器线程不安全(Map相当于缓存,统一管理节省资源);
HashTable线程安全,容器线程安全,但会影响性能(同步锁),不建议使用;
折中:ConCurrentHashMap(并非绝对线程安全);
8.10 单例设计模式——ThreadLocal线程单例
- ThreadLocal方法:不能保证单例全局唯一,能够保证线程唯一;
- 基于ThreadLocalMap;
ThreadLocal会给每个线程提供一个独立的变量副本; - 隔离了多个线程的访问冲突,用空间换时间;
8.11 单例模式源码分析(jdk + spring + mybatis)
- Runtime(饿汉式),Desktop(容器单例);
- spring的单例:基于容器,bean作用域中的单例,应用程序上下文;
单例设计模式:限制给定的类加载器管理的整个空间里;
spring: AbstractFactoryBean -> getObject - mybatis:ErrorContext,基于ThreadLocal保证了每个线程各自的数据;
- 单例模式是面试设计模式重点,应将本章中各种实现方案、优缺点、应用场景、源码使用、序列化破坏、反射攻击防御等都理解透。