多核cpu怎么保证数据一致性(四)volatile关键字、happens-before原则、内存屏障

系列文章目录

多核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的实现原理内存屏障。

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值