MST

星途 面试题库

面试题:Java内存模型中happens - before关系与指令重排序

在Java中,指令重排序会对happens - before关系产生影响。请详细说明在哪些情况下指令重排序会破坏原本基于happens - before关系的预期执行顺序,并给出相应的解决办法(比如使用关键字或其他机制)。
27.5万 热度难度
编程语言Java

知识考点

AI 面试

面试题答案

一键面试

指令重排序破坏happens - before预期执行顺序的情况

  1. 单线程内指令重排序:虽然单线程内最终结果符合程序顺序规则(单线程内代码按照编写顺序执行),但实际执行时编译器和处理器可能为了优化性能进行指令重排序。不过这种重排序不会影响单线程程序的最终执行结果。
  2. 多线程间指令重排序
    • 普通变量读写:多个线程访问共享普通变量时,由于指令重排序,可能导致一个线程对变量的修改,另一个线程不能及时看到。例如:
public class ReorderExample {
    int a = 0;
    boolean flag = false;

    public void writer() {
        a = 1;           // 1
        flag = true;      // 2
    }

    public void reader() {
        if (flag) {       // 3
            int i = a * a; // 4
        }
    }
}

在上述代码中,writer 方法里的 a = 1flag = true 可能会发生指令重排序。如果先执行 flag = true,此时另一个线程执行 reader 方法,由于 flagtrue,会执行 int i = a * a,但此时 a 可能还未赋值为 1,这就破坏了原本基于程序顺序的预期执行顺序。

解决办法

  1. 使用 volatile 关键字
    • 原理volatile 关键字保证了变量的可见性和禁止指令重排序。对 volatile 变量的写操作先行发生于后续对这个变量的读操作。对于上述例子,将 flag 声明为 volatile
public class ReorderExample {
    int a = 0;
    volatile boolean flag = false;

    public void writer() {
        a = 1;
        flag = true;
    }

    public void reader() {
        if (flag) {
            int i = a * a;
        }
    }
}

这样,a = 1 一定会在 flag = true 之前执行,且 writer 线程对 aflag 的修改对 reader 线程是可见的。 2. 使用 synchronized 关键字: - 原理synchronized 块对同一个锁的加锁和解锁操作具有 happens - before 关系。进入 synchronized 块的操作先行发生于该块内的操作,该块内的操作先行发生于退出 synchronized 块的操作。例如:

public class ReorderExample {
    int a = 0;
    boolean flag = false;
    final Object lock = new Object();

    public void writer() {
        synchronized (lock) {
            a = 1;
            flag = true;
        }
    }

    public void reader() {
        synchronized (lock) {
            if (flag) {
                int i = a * a;
            }
        }
    }
}

通过 synchronized 关键字,保证了 writerreader 方法内操作的顺序性和可见性。 3. 使用 java.util.concurrent.atomic 包下的原子类: - 原理:原子类如 AtomicInteger 等,内部使用了 Unsafe 类的 CAS(Compare - And - Swap)操作,这些操作具有原子性和内存可见性。例如,将 a 改为 AtomicInteger

import java.util.concurrent.atomic.AtomicInteger;

public class ReorderExample {
    AtomicInteger a = new AtomicInteger(0);
    boolean flag = false;

    public void writer() {
        a.set(1);
        flag = true;
    }

    public void reader() {
        if (flag) {
            int i = a.get() * a.get();
        }
    }
}

AtomicIntegersetget 方法保证了对 a 的操作的原子性和可见性,避免了指令重排序带来的问题。