likes
comments
collection
share

双重检查引出的创建对象原子性问题

作者站长头像
站长
· 阅读数 17

前言

单例模式的实现有:饿汉模式、懒汉模式、双重检测、静态内部类、枚举,但是双重检查可能会出现并发问题。

双重检查

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();
    }
}

双重检查的目的是解决懒汉模式加方法级别上加锁对性能的影响:

  1. 先判断是否为null,不为null就说明已经有线程使用过getInstance了,就可以直接返回单例了。
  2. 如果为null,就需要进入同步区(加了synchronized)创建了
  3. 进入同步区后还会判断一次,因为可能出现竞争
  4. 进行new单例

好像不会有啥问题,是的前期串行调用是没问题的,但是在高并发时可能会出现空指针异常(在调用getId的时候)。

instance = new IdGenerator()中的指令重排

步骤:

  1. 对内存开辟空间准备初始化对象
  2. 初始化对象
  3. 把对象地址赋值给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来保证(静态字段归所有类对象所共享,因此需要保证)。

从而实现懒加载,比懒汉模式更加高效(方法级别的同步,不管有没有创建都会同步阻塞)。