承接上一篇"并发的三个潜在问题:CPU缓存引发的可见性、线程切换引发的原子性、编译优化引发的有序性"。
本篇就来探寻一下Java并发编程中针对"可见性、有序性"提供的解决方案。
上期回顾
1
可见性:使用volatile禁用CPU缓存
我们在上一篇的分析中了解到,并发的可见性问题产生于各个CPU缓存中的数据副本与公共内存中的数据本体存在不一致的可能。
Java对于这个问题的解决思路是:提供关键字修饰的方式来主动的告知编译器“对于被修饰对象的处理,禁止使用CPU缓存,必须直接与内存交互”。
这样就避免了产生变量副本而与可见变量不一致的可能,这个关键字就是volatile
volatile int A = 0;
使用示例
2
有序性:Java内存模型的happens-before原则
什么是Java内存模型--Java Memory Model,JMM?
在了解happens-fefore原则之前需要先了解JMM,它可以理解成是一种虚拟机的规范,它规定了一个线程对共享变量的写入何时对另一个线程可见。
从抽象的角度来看,JMM定义了线程和主内存之间的抽象关系:线程之间的共享变量存储在主内存(main memory)中,每个线程都有一个私有的本地内存(local memory),本地内存中存储了该线程以读/写共享变量的副本。
还记得这个图片吗,不管是在上一篇文章还是在前文中都有出现过,这种共享内存的并发模型就是一种JMM。
优化的JMM:happens-before原则
先看下面这段代码,假设现在有两个线程A和B,A 执行 writer() 方法,按照上文 volatile 语义,会把变量 “v=true” 直接写入内存。
假设线程 B 执行 reader() 方法,同样按照 volatile 语义,线程 B 会直接从内存中读取变量 v。
如果线程 B 看到 “v == true” 时,那么线程 B 看到的变量 x 是多少呢?
class VolatileExample {
int x = 0;
volatile boolean v = false;
public void writer() {
x = 1;
v = true;
}
public void reader() {
if (v == true) {
System.out.println("x=" + x);
}
}
}
由于x我们并未使用volatile 修饰,CPU缓存和内存之间存在数据一致性问题,那么X的值0、1都有可能。
于是从Java1.5开始,对JMM增加了happens-before原则来解决这个问题。
happens-before原则
1、程序的顺序性规则:一个线程中,按照程序顺序,前面的操作 Happens-Before 于后续的任意操作。
2、volatile域规则:对一个volatile域的写操作,happens-before于任意线程后续对这个volatile域的读操作。
3、传递性规则:如果 A happens-before B,且 B happens-before C,那么A happens-before C。
通过这三条规则我们可以对上面的代码做这样的推断:
首先“x=42” Happens-Before 写变量 “v=true” --原则1
写变量“v=true” Happens-Before 读变量 “v=true” --原则2
“x=42” Happens-Before 读变量“v=true” --原则3
分析到这里我们可以得出一个结论:如果某个语句使用了volatile修饰,那么该语句之前所有代码的执行结果都对其他线程可见。
所以从Java1.5开始,x的值一定是1,仔细体会一下,这就是happens-before的作用所在。
以上就是本篇的内容,总结一下涉及的知识点:volatile的作用、Java内存模型--JMM的概念、happens-before原则的作用。浩说编程,帮你学到更多!