本文最后更新于:2021-06-16T07:25:35+00:00
记录一下双检锁单例模式是怎么一回事
封面 pixiv id 64535234
0x01
懒汉单例模式是无法保证线程安全的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| public class Singleton { private static Singleton singleton;
private Singleton() { }
public static Singleton getInstance() { if (singleton == null) { singleton = new Singleton(); }
return singleton; } }
|
即当线程A通过if判断,但还未创建实例,此时线程B也能够通过if判断,那么就会对重复创建实例违反单例的初衷。
0x02
那么我们为了保证线程安全,引入synchronized进行同步处理
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| public class Singleton { private static Singleton singleton;
private Singleton() { }
public static synchronized Singleton getInstance() { if (singleton == null) { singleton = new Singleton(); } return singleton; } }
|
此时就只有一个线程能够获取锁进入到if判断创建实例了。
- synchronized 偏向锁,自旋锁,轻量级锁,重量级锁
通过 synchronized 加锁,第一个线程获取的锁为偏向锁,这时有其他线程参与锁竞争,升级为轻量级锁,其他线程通过循环的方式尝试获得锁,称自旋锁。若果自旋的次数达到一定的阈值,则升级为重量级锁。
需要注意的是,在第二个线程获取锁时,会先判断第一个线程是否仍然存活,如果不存活,不会升级为轻量级锁。
0x03
可是现在由于同步处理导致每次获取实例都需要竞争获取锁导致效率非常低下,
所以我们应该在最外面做一次if判断来让大多数时候直接return实例而不是进行锁的竞争。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| public class Singleton { private static Singleton singleton;
private Singleton() { }
public static Singleton getInstance() { if (singleton == null) { synchronized (Singleton.class) { if (singleton == null) { singleton = new Singleton(); } } } return singleton; } }
|
而这就是双检锁(双重检查锁,Double check lock,DCL)。
0x04
但这里其实还是没有彻底解决多线程的问题,因为new Object分为三个步骤:
- 分配内存空间
- 初始化对象信息
- 将内存空间引用赋值给变量
如果这当中指令重排了,在还没有初始化对象的时候就把地址赋值给了变量,此时在最外层的if判断变量不为空,因为有地址,
这时候就会拿到一个未经初始化的变量。
所以我们还需要用volatile修饰变量
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| public class Singleton { private static volatile Singleton singleton;
private Singleton() { }
public static Singleton getInstance() { if (singleton == null) { synchronized (Singleton.class) { if (singleton == null) { singleton = new Singleton(); } } } return singleton; } }
|
在多线程环境下,保证变量的可见性。使用了 volatile 修饰变量后,在变量修改后会立即同步到主存中,每次用这个变量前会从主存刷新。
禁止 JVM 指令重排序。