双重检查引出的创建对象原子性问题
前言
单例模式的实现有:饿汉模式、懒汉模式、双重检测、静态内部类、枚举,但是双重检查可能会出现并发问题。
双重检查
package com.study.designmode.singleton.doublecheck;
import java.util.concurrent.atomic.AtomicLong;
public class IdGenerator {
private AtomicLong id = new AtomicLong(0);
private static IdGenerator instance;
private IdGenerator() {
}
public static IdGenerator getInstance() {
if (instance == null) {
synchronized (IdGenerator.class) {
if (instance == null) {
instance = new IdGenerator();
}
}
}
return instance;
}
public long getId() {
return id.incrementAndGet();
}
}
双重检查的目的是解决懒汉模式加方法级别上加锁对性能的影响:
- 先判断是否为null,不为null就说明已经有线程使用过getInstance了,就可以直接返回单例了。
- 如果为null,就需要进入同步区(加了synchronized)创建了
- 进入同步区后还会判断一次,因为可能出现竞争
- 进行new单例
好像不会有啥问题,是的前期串行调用是没问题的,但是在高并发时可能会出现空指针异常(在调用getId的时候)。
instance = new IdGenerator()中的指令重排
步骤:
- 对内存开辟空间准备初始化对象
- 初始化对象
- 把对象地址赋值给instance引用
第二、三步强依赖第一步,但是二三步没有依赖,因此会出现指令重排,单线程下是没问题的,多线程就会出现上面说的空指针问题。
复现问题
jdk 1.8
@SneakyThrows
public static void main(String[] args) {
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(16, 32, 0, TimeUnit.SECONDS, new LinkedBlockingQueue<>(16));
for (int i = 0; i < 48; i++) {
threadPoolExecutor.execute(new Runnable() {
@Override
public void run() {
System.out.println(getInstance().getId());
}
});
if (i > 3) {
Thread.sleep(1);
}
}
threadPoolExecutor.shutdown();
}
模拟多线程,期望能碰上先执行第三步再出现第二步,加Thread.sleep的原因是怕一起发起会出现所有线程都阻塞在同步区外,这样就会等到第二、三步都执行,sleep 1毫秒等执行第一步和第三步,等待区间0-43毫秒,该时间范围内应该能执行完开辟内存空间+赋值给引用,但是执行多次没碰上,因为具体执行时间比较难估算、执行其他代码也需要时间且难估算,碰上存在随机性较大。
字节码
看看字节码的指令顺序是啥
使用javap -v -p
查看字节码:
public static com.study.designmode.singleton.doublecheck.IdGenerator getInstance();
descriptor: ()Lcom/study/designmode/singleton/doublecheck/IdGenerator;
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=2, args_size=0
//从类中获取static字段-instance
0: getstatic #5 // Field instance:Lcom/study/designmode/singleton/doublecheck/IdGenerator;
// 如果引用(instance)不为空则走下面分支
3: ifnonnull 37
// 将IdGenerator类从运行时常量池推送至栈顶
6: ldc #6 // class com/study/designmode/singleton/doublecheck/IdGenerator
// 复制操作栈顶值并将复制值压入栈顶-IdGenerator类
8: dup
// 将操作数栈顶引用型数值(IdGenerator类)存入第一个本地变量
9: astore_0
// 获取对象锁
10: monitorenter
// 从类中获取static字段-instance
11: getstatic #5 // Field instance:Lcom/study/designmode/singleton/doublecheck/IdGenerator;
// 如果引用(instance)不为空则走下面分支
14: ifnonnull 27
// 创建一个新对象
17: new #6 // class com/study/designmode/singleton/doublecheck/IdGenerator
// 复制操作栈顶值并将复制值压入栈顶-IdGenerator类
20: dup
// 调用实例方法进行初始化
21: invokespecial #7 // Method "<init>":()V
// 在类中设置静态字段
24: putstatic #5 // Field instance:Lcom/study/designmode/singleton/doublecheck/IdGenerator;
// 将指定的对象引用本地变量推送至操作数栈顶
27: aload_0
28: monitorexit
29: goto 37
32: astore_1
33: aload_0
34: monitorexit
35: aload_1
36: athrow
37: getstatic #5 // Field instance:Lcom/study/designmode/singleton/doublecheck/IdGenerator;
// 从方法中返回引用
40: areturn
可以看到和前面说的步骤一样,使用jdk17编译看看:
public static com.study.designmode.singleton.doublecheck.IdGenerator getInstance();
descriptor: ()Lcom/study/designmode/singleton/doublecheck/IdGenerator;
flags: (0x0009) ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=2, args_size=0
0: getstatic #18 // Field instance:Lcom/study/designmode/singleton/doublecheck/IdGenerator;
3: ifnonnull 37
6: ldc #13 // class com/study/designmode/singleton/doublecheck/IdGenerator
8: dup
9: astore_0
10: monitorenter
11: getstatic #18 // Field instance:Lcom/study/designmode/singleton/doublecheck/IdGenerator;
14: ifnonnull 27
17: new #13 // class com/study/designmode/singleton/doublecheck/IdGenerator
20: dup
21: invokespecial #22 // Method "<init>":()V
24: putstatic #18 // Field instance:Lcom/study/designmode/singleton/doublecheck/IdGenerator;
27: aload_0
28: monitorexit
29: goto 37
32: astore_1
33: aload_0
34: monitorexit
35: aload_1
36: athrow
37: getstatic #18 // Field instance:Lcom/study/designmode/singleton/doublecheck/IdGenerator;
40: areturn
是一样的。
解决问题
volatile
修改一行即可:
private static volatile IdGenerator instance;
volatile有两个作用:可见性、防止指令重排。
会在读写操作前后加入内存屏障。
静态内部类
package com.study.designmode.singleton.staticinnerclass;
import java.util.concurrent.atomic.AtomicLong;
public class IdGenerator {
private AtomicLong id = new AtomicLong(0);
private IdGenerator() {
}
/**
* 内部静态类,在IdGenerator被加载的谁会,并不会创建SigngleHolder实例对象,只有调用getInstance时才会
*/
private static class SingletonHolder {
private static final IdGenerator instance = new IdGenerator();
}
public static IdGenerator getInstance() {
return SingletonHolder.instance;
}
public long getId() {
return id.incrementAndGet();
}
}
利用的是内部静态类不会随外部类加载而创建对象,instance的唯一性、创建过程的线程安全性,都由JVM来保证(静态字段归所有类对象所共享,因此需要保证)。
从而实现懒加载,比懒汉模式更加高效(方法级别的同步,不管有没有创建都会同步阻塞)。
转载自:https://juejin.cn/post/7216340356950097980