上一节中讲了可见性,原子性和有序性问题产生的源头。这一节我们就来讲讲如何解决可见性,有序性问题。
通过上节我们知道可见性是由于CPU缓存引起的,有序性是由于编译器重排优化引起的。
那么解决可见性和有序性最简单的方案就是禁用CPU缓存和禁止指令重排。但是如果我们直接粗暴的禁止也就享受不了CPU缓存和指令重排带来的性能优化的成果了。最好的办法是能够提供禁止使用CPU缓存和指令重排的方法,由我们程序员自己决定什么时候禁止这些。那么有没有这些方法呢?庆幸的是java已经为我们考虑了这点。也就是我们接下来要讲的JMM。
一、java内存模型(JMM)
说起java内存模型,大家可能首先联想到的是java的内存结构(JVM内存结构)。很多人误以为两者是一回事,其实不然。
JMM并不像JVM内存结构真实存在的,它是一个抽象概念。JMM是和多线程相关的,他描述了一组规范,主要是围绕解决在并发过程中如何处理可见性、原子性和有序性来建立的。它定义了一些语法集,映射到java语言就是volatile、synchronized和final三个关键字,以及六项Happens-Before规则。本质上可以理解为JMM规范了如何提供按需禁止CPU缓存和指令重排的方法。
一、volatile关键字的作用
当用volatile来声明一个变量时候
volatile int a=0
它意味着:告诉编译器,对于这个变量的读写不能使用cpu缓存。必须从内存中读取和写入。同时禁用指令重排
二、Happens-Before规则
这个不要望文生义(先行发生),它要表达的意思是:前面一个操作的结果对后续操作是可见的。
1、程序的顺序性规则
同一线程中前面的操作Happens-Before后面的操作。保证了程序的顺序执行,即我们编写的代码前面的语句Happens-Before后面的语句
int a=1;
int b=1;
P p = new P();
这里可能会有个疑问:程序的顺序性规则是否意味着就全面禁用了指令重排呢?个人理解是不会的。这里规则保证的是高级语言的一条语句的action Happens-Before后面的语句。例如这里的第3句,会对应多条CPU指令。1.申请内存,2.初始化,3.绑定变量地址。这里的2,3是没有先后依赖关系的可以进行指令重排
2、volatile变量规则
对volatile字段的写入操作Happens-Before于后续每一个读操作
3、传递性
如果A动作Happens-Before B动作,B动作Happens-Before C动作,那么A Happens-Before C
4、管程中锁的规则
对于一个锁的解锁Happens-Before于后续对这个锁的加锁
5、线程start()规则
它是指主线程A启动子线程B后,B线程能够看到主线程在启动子线程B前的操作当然指的是共享变量。
public void A(){
x=0;
Thread B = new Thread();
B.start();
}
B线程能够看到A方法启动B之前所有的操作
6、线程join()规则
它是指主线程A等待子线程B完成,当子线程B完成后。主线程A能够看到子线程B的操作。
public void A(){
Thread B = new Thread(
public void run(){
x=0;
}
);
B.start();
B.join();
}
为了更好的理解这6条规则,大家看看如下会是什么值?为什么?
public class A{
private int x=0;
private volatile y =0;
public void write(){
x=1;
y=2;
}
public void read(){
if(y ==2){
//x的值是多少
}
}
}
![公众号](https://i-blog.csdnimg.cn/blog_migrate/b4dde1e50a5e58e5abe92445adf36016.jpeg)