并发编程解决单例模式
一、概述
定义:如果一个类始终只能创建一个实例,那么这个类被称为单例类,这种设计模式被称为单例模式。
这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。
注意:
- 单例类只能有一个实例。
- 单例类必须自己创建自己的唯一实例。
- 单例类必须给所有其他对象提供这一实例。
关于具体的单例模式,可以参考我写的这篇文章:单例模式
二、懒汉式单例
为了在多线程环境下保护懒汉式,需要加上 synchronized 锁
1 |
|
说明
多线程同时调用getInstance()
, 如果不加synchronized锁, 此时两个线程同时判断INSTANCE为空, 此时都会new Singleton()
, 此时就不再符合单例模式。所以要加锁,防止多线程操作共享资源造成的安全问题。
同时,上面代码的效率也存在很大问题,当成功创建一个单例对象后,又来一个线程在执行获取锁时,还是会加锁,再次进行判断INSTANCE==null
,此时INSTANCE
肯定不为null,然后就返回刚才创建的INSTANCE。这样做会严重影响性能。
双重检查锁优化
1 |
|
以上的实现特点是
- 懒惰实例化
- 首次使用
getInstance()
才使用 synchronized 加锁,后续使用时无需加锁 - 有隐含的,但很关键的一点:第一个 if 使用了 INSTANCE 变量,是在同步块之外
三、代码存在的问题
但在多线程环境下,上面的代码是有问题的,getInstance
方法对应的字节码为:
1 |
|
其中
- 17 表示创建对象,将对象引用入栈 // new Singleton
- 20 表示复制一份对象引用 // 引用地址
- 21 表示利用一个对象引用,调用构造方法
- 24 表示利用一个对象引用,赋值给 static INSTANCE
也许 jvm 会优化为:先执行 24,再执行 21。
如果两个线程 t1,t2 按如下时间序列执行:
- 关键在于 0: getstatic 这行代码在 monitor 控制之外,可以越过 monitor 读取 INSTANCE 变量的值
- 这时 t1 还未完全将构造方法执行完毕,如果在构造方法中要执行很多初始化操作,那么 t2 拿到的是将是一个未初始化完毕的单例 对 INSTANCE 使用 volatile 修饰即可,可以禁用指令重排。
- 注意在 JDK 5 以上的版本的 volatile 才会真正有效
四、volatile解决方案
1 |
|
字节码上看不出来 volatile 指令的效果
1 |
|
如上面的注释内容所示,读写 volatile 变量时会加入内存屏障(Memory Barrier(Memory Fence)),保证下面两点:
- 可见性
- 写屏障(sfence)保证在该屏障之前的 t1 对共享变量的改动,都同步到主存当中
- 而读屏障(lfence)保证在该屏障之后 t2 对共享变量的读取,加载的是主存中最新数据
- 有序性
- 写屏障会确保指令重排序时,不会将写屏障之前的代码排在写屏障之后
- 读屏障会确保指令重排序时,不会将读屏障之后的代码排在读屏障之前
- 更底层是读写变量时使用 lock 指令来多核 CPU 之间的可见性与有序性
加上volatile
之后, 保证了指令的有序性
, 不会发生指令重排, 21就不会跑到24之后执行了
- synchronized 既能保证原子性、可见性、有序性,其中有序性是在该共享变量完全被synchronized 所接管(包括共享变量的读写操作),上面的例子中synchronized 外面的 if (INSTANCE == null) 中的INSTANCE读操作没有被synchronized 接管,因此无法保证INSTANCE共享变量的有序性(即不能防止指令重排)。
- 对共享变量加volatile关键字可以保证可见性和有序性,但是不能保证原子性(即不能防止指令交错)。
并发编程解决单例模式
http://example.com/2023/03/29/并发编程解决单例模式/