系列文章目录
多核cpu怎么保证数据一致性(一)为什么要做指令重排序?
多核cpu怎么保证数据一致性(二)cpu为什么要用高速缓存?L1,L2,L3 cache
多核cpu怎么保证数据一致性(三)MESI缓存一致性协议
多核cpu怎么保证数据一致性(四)volatile关键字、happens-before原则、内存屏障
前言
上篇文章《多核cpu怎么保证数据一致性(三)MESI缓存一致性协议》我们说了怎么保证缓存的一致性的MESI协议,这样的情况下cpu层面是不会出现缓存不一致的问题的,那么java为什么还有volatile等关键字来保证缓存的一致性呢。
一、java的数据不一致问题
如下一段代码
public class VolatileTest {
private static int COUNTER = 0;
public static void main(String[] args) {
new ChangeListener().start();
new ChangeMaker().start();
}
static class ChangeListener extends Thread {
@Override
public void run() {
int threadValue = COUNTER;
while ( threadValue < 5){
if( threadValue!= COUNTER){
System.out.println("Got Change for COUNTER : " + COUNTER + "");
threadValue= COUNTER;
}
}
}
}
static class ChangeMaker extends Thread{
@Override
public void run() {
int threadValue = COUNTER;
while (COUNTER <5){
System.out.println("Incrementing COUNTER to : " + (threadValue+1) + "");
COUNTER = ++threadValue;
try {
Thread.sleep(500);
} catch (InterruptedException e) { e.printStackTrace(); }
}
}
}
}
控制台打印结果如下:
Incrementing COUNTER to : 1
Got Change for COUNTER : 1
Incrementing COUNTER to : 2
Incrementing COUNTER to : 3
Incrementing COUNTER to : 4
Incrementing COUNTER to : 5
可以发现在ChangeListener中只打印了一次,也就是后面COUNTER变量的变更,在ChangeListener中感知不到了,如果我们把COUNTER变量的值改为volatile的,则打印结果如下:
Incrementing COUNTER to : 1
Got Change for COUNTER : 1
Incrementing COUNTER to : 2
Got Change for COUNTER : 2
Incrementing COUNTER to : 3
Got Change for COUNTER : 3
Incrementing COUNTER to : 4
Got Change for COUNTER : 4
Incrementing COUNTER to : 5
Got Change for COUNTER : 5
可以看到volatile关键字加上后,ChangeListener线程中可以感知到变更了。
我们知道volatile关键字加上后,会直接去主存读取数据,而不加volatile关键字的时候,其实是java编译器为了提升性能做了编译上的特殊处理。
二、内存屏障
加上volatile关键字之后,java编译器都做了哪些特殊处理呢?
其实是在cpu层面加了一层内存屏障,关于内存屏障的详细介绍,我就不多做介绍了,有兴趣的朋友可以看此文章《java内存屏障的原理与应用》
三、happens-before原则
在java中,指令重排序有个happens-before原则,就是通过内存屏障实现的,happens-before原则如下:
-
程序次序规则: 在一个单独的线程中,按照程序代码的执行流顺序,(时间上)先执行的操作happen—before(时间上)后执行的操作
(同一个线程中前面的所有写操作对后面的操作可见) -
管理锁定规则:一个unlock操作happen—before后面(时间上的先后顺序)对同一个锁的lock操作。
(如果线程1解锁了monitor a,接着线程2锁定了a,那么,线程1解锁a之前的写操作都对线程2可见(线程1和线程2可以是同一个线程)) -
volatile变量规则:对一个volatile变量的写操作happen—before后面(时间上)对该变量的读操作。
(如果线程1写入了volatile变量v(临界资源),接着线程2读取了v,那么,线程1写入v及之前的写操作都对线程2可见(线程1和线程2可以是同一个线程)) -
线程启动规则:Thread.start()方法happen—before调用用start的线程前的每一个操作。
(假定线程A在执行过程中,通过执行ThreadB.start()来启动线程B,那么线程A对共享变量的修改在接下来线程B开始执行前对线程B可见。注意:线程B启动之后,线程A在对变量修改线程B未必可见。) -
线程终止规则:线程的所有操作都happen—before对此线程的终止检测,可以通过Thread.join()方法结束、Thread.isAlive()的返回值等手段检测到线程已经终止执行。
(线程t1写入的所有变量,在任意其它线程t2调用t1.join(),或者t1.isAlive() 成功返回后,都对t2可见。) -
线程中断规则:对线程interrupt()的调用 happen—before 发生于被中断线程的代码检测到中断时事件的发生。
(线程t1写入的所有变量,调用Thread.interrupt(),被打断的线程t2,可以看到t1的全部操作) -
对象终结规则:一个对象的初始化完成(构造函数执行结束)happen—before它的finalize()方法的开始。
(对象调用finalize()方法时,对象初始化完成的任意操作,同步到全部主存同步到全部cache。) -
传递性:如果操作A happen—before操作B,操作B happen—before操作C,那么可以得出A happen—before操作C。
(A h-b B , B h-b C 那么可以得到 A h-b C)
happens-before只是一个规则,实现的原理就是我们说的内存屏障。
总结
今天我们说了happens-before原则、volatile的实现原理内存屏障。