重点:
1.模式定义/应用场景/类图分析
2.字节码知识/字节码指令重排
3.类加载机制
4.JVM序列化机制
5.单例模式在Spring框架 & JDK源码中的应用
模式定义:保证一个类只有一个实例,并且只提供一个全局访问点
使用场景:重量级的对象,不需要多个实例,如线程池,数据库连接池
![a477f85929bb67cca2bfca5eb6f4d7a6.png](https://img-blog.csdnimg.cn/img_convert/a477f85929bb67cca2bfca5eb6f4d7a6.png)
单例模式UML类图
1. 懒汉模式:延迟加载,只有真正使用的时候,才开始实例化
1)线程安全问题
2)double check 加锁优化
3)JIT编译器,CPU有可能对指令进行重排,导致使用到尚未初始化的实例,可以通过添加volatile关键字修饰解决。即volatile修饰的字段能够防止指令重排。
单线程环境下的实现:
public class LazyInstance { public static void main(String[] args) { LazySingleton lazySingleton1 = LazySingleton.getInstance(); LazySingleton lazySingleton2 = LazySingleton.getInstance(); System.out.println(lazySingleton2 == lazySingleton1); }}class LazySingleton { private static LazySingleton instance; private LazySingleton() { } public static LazySingleton getInstance() { if (instance == null) { instance = new LazySingleton(); } return instance; }}
![2acd77c710b191c740f7f2b0a4aab45d.png](https://img-blog.csdnimg.cn/img_convert/2acd77c710b191c740f7f2b0a4aab45d.png)
多线程环境下全局访问方法getInstance()不加锁:
public class ConcurrentLazyInstance { public static void main(String[] args) { new Thread(() -> { LazySingleton instance = LazySingleton.getInstance(); System.out.println("Thread1instance = " + instance); }).start(); new Thread(() -> { LazySingleton instance = LazySingleton.getInstance(); System.out.println("Thread2instance = " + instance); }).start(); }}class LazySingleton { private static LazySingleton instance; private LazySingleton() { } public static LazySingleton getInstance() { if (instance == null) { instance = new LazySingleton(); } return instance; }}
![24701df7f83758e636cc22bca8bd244c.png](https://img-blog.csdnimg.cn/img_convert/24701df7f83758e636cc22bca8bd244c.png)
破坏了单例的定义,产生了两个对象
多线程环境下全局访问方法getInstance()加锁:
public class ConcurrentLazyInstance { public static void main(String[] args) { new Thread(() -> { LazySingleton instance = LazySingleton.getInstance(); System.out.println("Thread1instance = " + instance); }).start(); new Thread(() -> { LazySingleton instance = LazySingleton.getInstance(); System.out.println("Thread2instance = " + instance); }).start(); }}class LazySingleton { private static LazySingleton instance; private LazySingleton() { } public synchronized static LazySingleton getInstance() { if (instance == null) { instance = new LazySingleton(); } return instance; }}
![2975eca5a033d0e0b04ef4804de960ef.png](https://img-blog.csdnimg.cn/img_convert/2975eca5a033d0e0b04ef4804de960ef.png)
通过synchronize锁住方法,保证单例,但性能会有所下降
为了提高性能,getInstance()方法我们采用双重校验锁
public static LazySingleton getInstance() { //先判断对象是否已经实例过,没有实例化过才进入加锁代码 if (instance == null) { //类对象加锁 synchronized (LazySingleton.class) { if (instance == null) { instance = new LazySingleton(); } } } return instance; }}
另外,需要注意 instance 采用 volatile 关键字修饰也是很有必要。
instance 采用 volatile 关键字修饰也是很有必要的, instance = new LazySingleton(); 这段代码其实是分
为三步执行:
1). 为 instance 分配内存空间
2). 初始化 instance
3). 将 instance 指向分配的内存地址
但是由于 JVM 具有指令重排的特性,执行顺序有可能变成 1->3->2。指令重排在单线程环境下不会出现问题,但是在
多线程环境下会导致一个线程获得还没有初始化的实例。例如,线程 T1 执行了 1 和 3,此时 T2 调用
getInstance() 后发现 instance 不为空,因此返回 instance,但此时 instance 还未被
初始化。
使用 volatile 可以禁止 JVM 的指令重排,保证在多线程环境下也能正常运行。
- 饿汉式:JVM类加载的初始化阶段就完成了实例化的初始化。本质上借助类加载机制,保证实例的唯一性。
类加载的过程:
1)加载:二进制数据加载到内存,生成对应的Class数据结构
2)连接:a.验证,b.准备(给类的静态成员变量赋默认值),c.解析
3)初始化:给类的静态变量赋初值
单线程环境下的实现饿汉式:
public class HungrySingletonTest { public static void main(String[] args) { HungrySingleton lazySingleton1 = HungrySingleton.getInstance(); HungrySingleton lazySingleton2 = HungrySingleton.getInstance(); System.out.println(lazySingleton2 == lazySingleton1); }}class HungrySingleton { private static HungrySingleton instance = new HungrySingleton(); private HungrySingleton() { } public static HungrySingleton getInstance() { return instance; }}
- 静态内部类
1)本质上是利用类的加载机制保证线程线程安全
2)当在实际使用的时候才会触发类的初始化。所以也是懒加载的一种形式
class InnerClassSingleton { private static class InnerClassHolder { private static InnerClassSingleton instance = new InnerClassSingleton().getInstance(); } private InnerClassSingleton() { } public static InnerClassSingleton getInstance() { return InnerClassHolder.instance; }}
- 反射攻击
在饿汉模式和静态内部类初始化情况下,反射实例化和单例实例化不是一个对象
public class HungrySingletonTest { public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException { //反射实例化和单例实例化不是一个对象 //如何防止?在饿汉模式和静态内部类的初始化构造函数中判断类的实例是否为null,不等于null则抛出异常 Constructor declaredConstructor = InnerClassSingleton.class.getDeclaredConstructor(); declaredConstructor.setAccessible(true); InnerClassSingleton innerClassSingleton = declaredConstructor.newInstance(); InnerClassSingleton instance3 = InnerClassSingleton.getInstance(); System.out.println(instance3==innerClassSingleton); }}class HungrySingleton { private static HungrySingleton instance = new HungrySingleton(); private HungrySingleton() { } public static HungrySingleton getInstance() { return instance; }}class InnerClassSingleton { private static class InnerClassHolder { private static InnerClassSingleton instance = new InnerClassSingleton(); } private InnerClassSingleton() { if(InnerClassHolder.instance != null){ //在饿汉模式和静态内部类的初始化构造函数中判断类的实例是否为null,不等于null则抛出异常 throw new RuntimeException("单例不允许多个实例"); } } public static InnerClassSingleton getInstance() { return InnerClassHolder.instance; }}
如何防止?答:在饿汉模式和静态内部类的初始化构造函数中判断类的实例是否为null,不等于null则抛出异常
- 枚举类型
1) 天然不支持反射创建对应实例,且有自己的反序列化机制
2) 利用类加载机制保证线程安全
public enum EnumsSingleton { INSTANCE; public void print() { System.out.println("this.hashCode() = " + this.hashCode()); }}class EnumTest { public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException { /** * EnumsSingleton instance = EnumsSingleton.INSTANCE; EnumsSingleton instance1 = EnumsSingleton.INSTANCE; System.out.println(instance==instance1); */ Constructor declaredConstructor = EnumsSingleton.class.getDeclaredConstructor(String.class, int.class); declaredConstructor.setAccessible(true); EnumsSingleton enumsSingleton = declaredConstructor.newInstance("INSTANCE", 0); }}
![f297b64da2a2c2645a527c261d0d0957.png](https://img-blog.csdnimg.cn/img_convert/f297b64da2a2c2645a527c261d0d0957.png)
enum天然不支持反射创建对应实例
- 通过InnerClassSingleton将instance4实例序列化到磁盘上,在项目的根目录,再从磁盘反序列化到内存中,读出对象,比较放进去的对象和我们读出来的对象是不是一个对象
public class HungrySingletonTest { public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException, IOException, ClassNotFoundException { //instance4实例序列化到磁盘上,在项目的根目录 InnerClassSingleton instance4 = InnerClassSingleton.getInstance(); ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("testSerializable")); oos.writeObject(instance4); oos.close(); //从磁盘反序列化到内存中,读出对象,比较放进去的对象和我们读出来的对象是不是一个对象 ObjectInputStream ois = new ObjectInputStream(new FileInputStream("testSerializable")); InnerClassSingleton object = (InnerClassSingleton) ois.readObject(); System.out.println(instance4 == object); }}class HungrySingleton { private static HungrySingleton instance = new HungrySingleton(); private HungrySingleton() { } public static HungrySingleton getInstance() { return instance; }}class InnerClassSingleton implements Serializable { private static class InnerClassHolder { private static InnerClassSingleton instance = new InnerClassSingleton(); } private InnerClassSingleton() { if (InnerClassHolder.instance != null) { throw new RuntimeException("单例不允许多个实例"); } } public static InnerClassSingleton getInstance() { return InnerClassHolder.instance; }}
![b8c7c5d29df6d591053b520975e12b8f.png](https://img-blog.csdnimg.cn/img_convert/b8c7c5d29df6d591053b520975e12b8f.png)
这是JVM序列化的一个机制,在源码中给出了解决方案:在InnerClassSingleton类实现readResolve()方法
Object readResolve() throws ObjectStreamException { return InnerClassHolder.instance; }
为了在序列化反序列化的时候保证数据是一致的,我们可以在类InnerClassSingleton中加入版本号:
static final long serialVersionUID = 42L;
能够保证写入的对象和读出的对象是同一个对象
![ac764a28cbb6868e3afac76a445b54b0.png](https://img-blog.csdnimg.cn/img_convert/ac764a28cbb6868e3afac76a445b54b0.png)
类似的,枚举是天然的保证序列化和反序列化为同一个对象
- 单例在源码中应用举例:
Runtime--->有饿汉模式
Currency ---->有享元模式(注意他的反序列化也是用readResolve)
DefaultSingletonBeanRegistry---->有双重校验锁
ReactiveAdapterRegistry---->有双重校验锁
ProxyFactoryBean
TomcatURLStreamHandlerFactory(Tomcat中)