指令重排对于Java开发者而言,是一个必须面对的问题,也要求我们在平时的工作中,对虚拟机底层有更加深入的理解和认识。为了更好的理解指令重排这件事,我将通过多线程的方式,复现指令重排以及最佳实践。

什么时候会发生指令重排?

先来一个测试指令重排现象,下面这段代码会发生指令重排,也就是JVM优化了执行顺序。

/**
 * 指令重排测试
 */
public class CommandDisorder {
    // 当使用volatile关键词修饰变量时,则不会出现指令重排现象
    private static /*volatile*/ int a = 0, b = 0, c = 0, d = 0;

    /**
     * 测试方式:一次开启两个线程,同时修改变量
     */
    public static void main(String[] args) throws InterruptedException {
        int i = 0;
        while (true) {
            i++;
            a = b = c = d = 0;
            Thread t1 = new Thread(() -> {
                a = 1;
                c = b; // 指令重排,会先执行这行代码,导致c = 0, d = 0
            });
            Thread t2 = new Thread(() -> {
                b = 1;
                d = a; // 指令重排,会先执行这行代码,导致c = 0, d = 0
            });
            t1.start();
            t2.start();
            t1.join();
            t2.join();
            if (c == 0 && d == 0) {
                System.err.println(String.format("第%s次出现指令重排", i));
                break;
            } else {
                System.out.println(i);
            }
        }
    }
}

指令重排,异常出现了:

img

什么是指令重排?

为了提高性能,编译器和处理器常常会对既定的代码执行顺序进行指令重排序,JMM内部会有指令重排,并且会有af-if-serial和happen-before的理念来保证指令的正确性。

  • af-if-serial:不管怎么重排序,单线程下的执行结果不能被改变;
  • 先行发生原则(happen-before):先行发生原则有很多,其中程序次序原则,在一个线程内,按照程序书写的顺序执行,书写在前面的操作先行发生于书写在后面的操作,准确地讲是控制流顺序而不是代码顺序。

真实的业务中如何避免指令重排?

在真实业务场景中,预测到可能有多线程访问同一个变量时,建议加上volatile关键词,保证变量在线程间的可见性。

举一个简单的例子,单例模式

public class Singleton {
    // 为了避免指令重排,这里需要加上volatile关键词
    private static /*volatile*/ Singleton singleton = null;

    /**
     * double check lock(DCL)
     */
    public static Singleton getInstance() {
        if (singleton == null) {
            synchronized (Singleton.class) {
                if (singleton == null) {
                    // new 一个对象的过程,有三个步骤
                    // 1.内存分配
                    // 2.初始化
                    // 3.返回对象引用
                    // 由于JVM指令重排优化,可能会使得2、3两步顺序发生变化,说明这里不是一个原子性操作
                    singleton = new Singleton();
                }
            }
        }
        return singleton;
    }

    private static void removeInstance() {
        singleton = null;
    }

    private final String field = "init";

    public static void main(String[] args) throws InterruptedException, ExecutionException {
        ExecutorService threadPool = Executors.newCachedThreadPool();

        long start = System.currentTimeMillis();
        for (int j = 0; j < 3000_000; j++) {
            execute(threadPool);
            Singleton.removeInstance();
        }

        System.out.println("正常结束");
        System.out.println("执行耗时:" + (System.currentTimeMillis() - start) + " ms");
        System.exit(0);
    }

    /**
     * 使用500个线程同时去获取实例
     */
    private static final int THREAD_COUNT = 500;
    private static void execute(ExecutorService threadPool) throws InterruptedException, ExecutionException {
        CountDownLatch downLatch = new CountDownLatch(THREAD_COUNT);
        List<Callable<Singleton>> list = new ArrayList<>();
        for (int i = 0; i < THREAD_COUNT; i++) {
            list.add(() -> {
                downLatch.countDown();
                Singleton instance = Singleton.getInstance();
                if (instance.field == null) {
                    throw new RuntimeException("获取到未实例化的对象");
                }
                return instance;
            });
        }
        List<Future<Singleton>> futures = threadPool.invokeAll(list);
        Set<Singleton> set = new HashSet<>();
        for (Future<Singleton> future : futures) {
            set.add(future.get());
        }
        if (set.size() > 1) {
            System.out.println("产生多实例!");
            throw new RuntimeException("产生多实例!");
        }
    }
}

当没有采用DCL时,可能会产生多实例。采用了DCL而没有使用volatile关键词,则可能出现:获取到未实例化的对象,原理见第一个示例。

看似一个简单的单例,内部却隐含了不少有意思的内容。

标签:

分类:

更新时间: