重排序
代码实际执行顺序和代码在 Java 文件中的顺序不一致,代码指令并不是严格按照代码语句顺序执行的,这就是重排序。
重排序的好处:提高处理速度
重排序前:
代码:
a = 3;
b = 2;
a = a + 1;
指令:
Load a
Set to 3
Store a
Load b
Set to 2
Store b
Load a
Set to 4
Store
重排序后:
代码:
a = 3;
a = a + 1;
b = 2;
指令:
Load a
Set to 3
Set to 4
Store a
Load b
Set to 2
Store b
很明显的看出,指令的数量变少了,但是结果是一致的。
重排序的 3 种情况
- 编译器优化:包括 JVM,JIT 编译器等。
- CPU 指令重排:就算编译器不发送重排,CPU 也有可能进行重排。
- 内存的“重排序”:线程A的修改线程B却看不见。比如线程A修改了a的值,之后到线程B执行,但是用的却是修改前 a 的值。
可见性
cpu 有多级缓存,导致读的数据过期。可见性问题不是由多核引起的,是由多缓存引起的。
在多线程情况下,线程A的修改线程B却看不见。因为每个 cpu 都有自己独立的工作内存,cpu 之间的信息交互靠主内存来解决。
volatile 的作用是当线程A修改了某个值,会将这个值的修改强制写入主内存,其他线程再去读取的时候,就会读到最新的值。
happens-before
含义:
- happens-before 规则是用来解决可见性问题的,在时间上,动作A发送在动作B之前,B保证能看见A。
- 如果一个操作 happens-before 于另一个操作,那么我们说第一个操作对于第二个操作是可见的。
happens-before 规则有哪些?
- 单线程原则。单线程情况下后面执行语句一定可以看得到前面语句执行的结果。
- 锁操作。线程A对某个变量加锁了,对于加锁之前变量的操作都是可见的。
- volatile 关键字。读取加了这个关键字的变量,一定能读到最新的结果,不可能读到历史数据。
- 线程启动。子线程启动的时候能看到,主线程中子线程启动前的所有结果。
- 线程 join。线程A等待线(join)程B执行完毕,那么B执行完毕之后,A继续执行的时候能看到B的所有操作。
- 传递性。第一行代码运行结果第二行能看到,第二行的结果第三行能看到,这么推到下去,第N行代码能看到之前所有运行结果。
- 中断。如果线程中断,那么一定会被检测到。
- 构造方法。 finalize() 方法执行的时候一定能看到构造方法的最后一条指令。
原子性
一系列的操作,要么全部执行,要么全部不执行,不会出现执行一半的情况,是不可分割的。
synchronized 是用来解决原子性这个问题的,保证多个操作要执行就都执行。 synchronized 不仅保证了原子性,还保证了可见性。
Java 中那些操作是具有原子性
- 除了 long 和 double 之外的基本类型(int, byte, boolean, short, char, float)的赋值操作。
- 对引用的赋值。
- java.concurrent.atomic.* 中的所有类的操作
java 中的 long 和 double 每次写入是拆分为2次 32 位的写入。在两次写入中间线程切换就会造成数据不对。最简单通用的解决方法就是加上 volatile 关键字。 商用的 java 虚拟机已经保证了 long 和 double 写入的原子性,因此我们仅仅需要知道 java 规范中没有约束这一点就够了。