第一部分:并发理论基础02->java内存模型,看java如何解决可见性和有序性问题
1.前情提要
可见性,原子性,有序性,称为并发编程的bug之源
2.java的内存模型
导致可见性问题是cpu缓存引起
导致有序性问题是编译器优化
那么解决方案是什么?
禁用缓存和禁用编译优化,但是程序性能就下降了
那么如何能保证性能的同时,又解决了可见性及有序性问题?
该禁用缓存和编译优化的时候禁用,其余时候不做限制
java内存模型规范了jvm如何按需禁用缓存和按需禁用编译优化的方法。
主要包含volatile,synchronized,final三个关键字,以及6个happens-before规则
3.volatile
volatile int x = 0
告诉编译器,对这个变量x的读写,不能使用cpu缓存,必须从内存中读取或写入
示例代码思考
// 以下代码来源于【参考1】
class VolatileExample {
int x = 0;
volatile boolean v = false;
public void writer() {
x = 42;
v = true;
}
public void reader() {
if (v == true) {
// 这里x会是多少呢?
}
}
}
线程A执行write方法,volatile会把变量v=true写入内存,假设线程B执行reader()方法,按照volatile,线程B会从内存中读取变量v,线程看到v==true时,变量x是多少?
jdk1.5之后是42,jdk1.5之后对volatile进行了升级
4.Happens-Before规则
前面一个操作的结果对后续操作是可见的
happens-before规则榆树了编译器的优化行为
4.1程序的顺序性规则
按照程序顺序,前面操作happens-before与后续任意操作。
x=42,happens-before与v=true,符合单线程里面的思维,程序前面对某个变量的修改一定是是对叙操作可见的
4.2 volatile规则
对volatile变量的写操作,happens-before与后续对这个volatile变量的读操作
有点禁用缓存的意思
4.3 传递性
A happens-before B,且B happens-before c,那么A happens-before c
x=42,happens-before与写变量v=true,这是4.1程序顺序性规则的内容
v=true happens-before 读变量v==true,这是4.2 volatile规则
根据传递性原则
x=52 happens-before 读变量vtrue
如果线程B读到了vtrue,那么线程A设置的x=42对线程B是可见的。
4.4 管程中锁的规则
锁的解锁happens-before与后续对这个锁的加锁操作
管程是java中提供的同步原语,就是synchronized关键字
synchronized (this) { //此处自动加锁
// x是共享变量,初始值=10
if (this.x < 12) {
this.x = 12;
}
} //此处自动解锁
结合4.4的规则,我们可以判断出如果x的初始值是10,线程A执行完代码库后x的值会变为12,然后自动释放锁,线程B抢到锁后,能够看到线程A对x的写操作,也就是说线程B可以看到x==12
4.5 线程start()规则
线程A调用线程B的start方法,那么该start操作happens-before与线程B中的任意操作
Thread B = new Thread(()->{
// 主线程调用B.start()之前
// 所有对共享变量的修改,此处皆可见
// 此例中,var==77
});
// 此处对共享变量var修改
var = 77;
// 主线程启动子线程
B.start();
4.6 线程join规则
线程等待,主线程A等待子线程B完成,调用join,子线程B完成后,主线程能够看到子线程的操作
看到的是对共享变量的操作
线程A调用线程B的join()并成功返回,那么线程B的任意操作happens-before 与该join操作的返回
Thread B = new Thread(()->{
// 此处对共享变量var修改
var = 66;
});
// 例如此处对共享变量修改,
// 则这个修改结果对线程B可见
// 主线程启动子线程
B.start();
B.join()
// 子线程所有对共享变量的修改
// 在主线程调用B.join()之后皆可见
// 此例中,var==66
5.final
final 关键字可以告诉编译器优化的更好一点
final修饰变量时,告诉编译器,这个变量生而不变,可劲优化
现在final 修饰的变量,对编译重排进行了约束,不会导致有序性问题并产生空指针异常
// 以下代码来源于【参考1】
final int x;
// 错误的构造函数
public FinalFieldExample() {
x = 3;
y = 4;
// 此处就是讲this逸出,
global.obj = this;
}
5.总结
happens-before 本质上是一种可见性
共享变量abc,在一个线程里设置了abc的值=3,那么有什么办法可以让其他线程看到abc==3
1.声明共享变量abc,并使用volatile关键字修饰
2.声明共享变量abc,在synchronized关键字对abc的赋值代码库加锁,由于happen-before管程锁规则,后续线程可以看到abc的值
3.A线程启动后,使用A.JOIN()方法来完成运行,后续线程再启动,就一定可以看到abc==3