0x01 volatile关键字
作用一:保证变量可见性
volatile
关键字修饰变量,保证此变量的写操作对所有线程的都是立即可见的。但是基于volatile
的运算操作不是立即可见的(非线程安全)
特殊
:以下两种场景需要要保证其操作的原子性
一:运算结果并不依赖变量的当前值,或者能够确保只有单一的线程修改变量的值
二:变量不需要与其他的状态变量共同参与不变约束
作用二:禁止指令重排序
在单线程中,所有代码都是被顺序执行的,因为就一个线程在执行这段代码,即时代码被调整了顺序,也没有其他线程因为这个被调整的顺序而产生错误。
通过代码举例说明
int i = 12;
boolean flag = false;
场景:如果此时有多个线程在执行这段代码,都是根据flag的状态来判断是否开始,而i
变量和flag
变量的顺序可能flag
变量先被赋值,那此时线程获取flag
是true
那么就会开始执行i
变量的操作,比如自增,那么此时对i
进行操作就是错误的。
原理
通过内存屏障
方式,即在编译指令中在赋值语句的下一句增加lock addl $0x0 (%esp)
这样的lock
操作来说明重排序时不能把后面的指令重排序到内存屏障之前的位置。虽然cpu有nop
这样的空操作指令,但是它不允许和lock
配合使用,所以每次lock
都是把ESP寄存器+0。
lock
前缀使得本CPU的Cache写入了内存,该写入动作会引起别的CPU或者别的内核缓存无效化(保证了可见性),相当于对Cache变量做了一次store
和write
操作
lock
指令把修改同步到内存时,意味着所有之前的操作都已经执行完成了
0x02 synchronized关键字
作用:可以解决原子性(加锁)、可见性(加锁)、有序性(as-if-serial)
实现原理
- synchronized方法:通过
ACC_SYNCHRONIZED
标记符来实现同步 - synchronized同步块:采用
monitorenter
和monitorexit
两个指令来实现
0x03 final关键字
如果没有发生this逃逸,那么final关键字能保证可见性,简单理解就是在任何线程访问某个对象的final域时,这个final域肯定被正确初始化了
什么是对象逃逸
在对象未完全初始化的时候,对象的引用被其他变量获取了
public class Foo {
int i;
static Foo foo;
Foo() {
i = 1; // 操作1
foo = this; // 操作2
}
}
上述代码中,操作1和操作2可能重排序,在单线程环境并无大碍,但是在多线程环境下,可能其他线程通过foo
变量去获取i
的值,但是由于重排序,可能i
还没被赋值
0x04 as-if-serial语义
该语义保证了单线程环境下重排序之后程序执行结果的正确性
int i = 1; // 操作1
int j = 2; // 操作2
int f = i * j; // 操作3
由此代码中,操作1和操作2可以重排序,但是操作3是根据操作1和操作2的值决定的,不能重排序到最前面
0x05 happens-before语义
作用:可以解决可见性、有序性
jvm要保证重排序是符合happens-before语义的,所以后者对前者是可见的
规则:
- 程序次序规则:在一个线程内,按照程序代码顺序,前面的操作先行发生于后面的操作
- 管程锁定规则:一个
unlock
操作先行发生于后面对同一个锁的lock
操作,同一个锁,后面指时间上的先后顺序 - volatile变量规则:对一个volatile变量的写操作先行发生于读操作
- 线程启动规则:Thread的start方法先行发生于此线程的每个动作
- 线程终止规则:线程中的所有操作都先行发生于对此线程的终止检测
- 线程中断规则:线程interrupt方法的调用先行发生于被中断线程的代码检测到中断事件的发生
- 对象终结规则:一个对象初始化完成(构造函数执行结束)先行发生于finalize方法
- 传递性:操作A先行于操作B,操作B先行于操作C,则操作A先行于操作C
0xFF 总结
jmm封装了底层操作,提供更为高级的api帮助我们解决并发的原子性、可见性、有序性问题