原子性
- 起因:多线程下,不同线程的指令发生了交错导致的共享变量的读写混乱
- 解决:用悲观锁或乐观锁解决,volatile 并不能解决原子性
可见性
- 起因:由于编译器优化、或缓存优化、或 CPU 指令重排序优化导致的对共享变量所做的修改另外的线程看不到
- 解决:用 volatile 修饰共享变量,能够防止编译器等优化发生,让一个线程对共享变量的修改对另一个线程可见
有序性
- 起因:由于编译器优化、或缓存优化、或 CPU 指令重排序优化导致指令的实际执行顺序与编写顺序不一致
- 解决:用 volatile 修饰共享变量会在读、写共享变量时加入不同的屏障,阻止其他读写操作越过屏障,从而达到阻止重排序的效果
- 注意:
- volatile 变量写加的屏障是阻止上方其它写操作越过屏障排到 volatile 变量写之下
- volatile 变量读加的屏障是阻止下方其它读操作越过屏障排到 volatile 变量读之上
- volatile 读写加入的屏障只能防止同一线程内的指令重排
也就是说对volatile变量的写放在方法内最后一行,对于volatile变量的读放在首行。
代码示例
原子性
public class AddAndSubtract {
static volatile int balance = 10;
public static void subtract() {
int b = balance; // 1
b -= 5; // 2
balance = b; //6
}
public static void add() {
int b = balance; //3
b += 5; //4
balance = b; //5
}
public static void main(String[] args) throws InterruptedException {
CountDownLatch latch = new CountDownLatch(2);
new Thread(() -> {
subtract();
latch.countDown();
}).start();
new Thread(() -> {
add();
latch.countDown();
}).start();
latch.await();
get().debug("{}", balance);
}
}
两个地方打上thread的断点。
最终数据结果为:
可见性
- 首先解释下为什么是由于编译器优化、或缓存优化、或 CPU 指令重排序优化导致指令的实际执行顺序与编写顺序不一致,我么先不用
volatile
关键字
public class ForeverLoop {
static boolean stop = false;
public static void main(String[] args) {
new Thread(() -> {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
stop = true;
get().debug("modify stop to true...");
}).start();
new Thread(() -> {
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
get().debug("{}", stop);
}).start();
new Thread(() -> {
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
get().debug("{}", stop);
}).start();
foo();
}
static void foo() {
int i = 0;
while (!stop) {
i++;
}
get().debug("stopped... c:{}", i);
}
}
运行结果发现,Thread-2
和 Thread-1
是可以读到Thread-0
的数据的。
接着证明是JIT编译器优化造成的,加上JVM运行参数-Xint
,接着在看运行结果,可以看到main线程是可以读到其他线程修改的值的。
最后,我们去掉JVM运行参数,然后加上volatile
关键字,看下运行结果,main线程也可以读到其他线程修改的值,说明了volatile
关键字是禁止了JIT编译器的优化实现的可见性。
为什么要指令重排
经过重排序之后,情况如下图所示:
可以看出,重排序后 a 的相关指令发生了变化,节省了一次 Load a 和一次 Store a。重排序通过减少执行指令,从而提高整体的运行速度,这就是重排序带来的优化和好处。
在执行程序时,为了提高性能,编译器和处理器常常会对指令做重排序。重排序分3种类型:
- 编译器优化的重排序。编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序。
- 指令级并行的重排序。现代处理器采用了指令级并行技术(Instruction-Level Parallelism,ILP)来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。
- 内存系统的重排序。由于处理器使用缓存和读/写缓冲区,这使得加载和存储操作看上去可能是在乱序执行。
既然重排序有这么好处,为什么还要禁止指令重排序?
在单线程程序中,对存在控制依赖的操作重排序,不会改变执行结果(这也是as-if-serial语义允许对存在控制依赖的操作做重排序的原因);但在多线程程序中,对存在控制依赖的操作重排序,可能会改变程序的执行结果。