面试题答案
一键面试指令重排序破坏happens - before预期执行顺序的情况
- 单线程内指令重排序:虽然单线程内最终结果符合程序顺序规则(单线程内代码按照编写顺序执行),但实际执行时编译器和处理器可能为了优化性能进行指令重排序。不过这种重排序不会影响单线程程序的最终执行结果。
- 多线程间指令重排序:
- 普通变量读写:多个线程访问共享普通变量时,由于指令重排序,可能导致一个线程对变量的修改,另一个线程不能及时看到。例如:
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 = 1
和 flag = true
可能会发生指令重排序。如果先执行 flag = true
,此时另一个线程执行 reader
方法,由于 flag
为 true
,会执行 int i = a * a
,但此时 a
可能还未赋值为 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
线程对 a
和 flag
的修改对 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
关键字,保证了 writer
和 reader
方法内操作的顺序性和可见性。
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();
}
}
}
AtomicInteger
的 set
和 get
方法保证了对 a
的操作的原子性和可见性,避免了指令重排序带来的问题。