volatile、JMM、内存屏障

初识Volatile

    private static volatile boolean stop = false;

    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(() -> {
            int i = 0;
            while (!stop){		//(1) 读
                i++;
            }
        });
        thread.start();
        Thread.sleep(1000);
        stop = true;			//(2) 写
    }

上面的代码,如果stop变量没有被volatile修饰的话,线程是不会被终止的,只有加上volatile,线程才会退出。

分析:

多线程环境下,读线程不能及时的获取到其他线程写入的最新的值,这就是所谓的可见性问题。

硬件层面分析可见性问题

众所周知,CPU、内存和IO设备之间存在速度的不匹配,为了平衡三者之间的速度差异,最大化的利用CPU提升性能,硬件、操作系统、编译器等方面都做出了很多的优化

CPU增加高速缓存

解决了处理器和内存的速度矛盾,但是会产生了新的问题,缓存一致性

​ 由于每个线程都有自己的高速缓存,同一份数据可能被缓存到不同的CPU中,如果在不同的CPU中运行的不同线程看到同一份内存的缓存值不一样就会存在缓存不一致的问题。

​ CPU层面的解决办法:

(1)总线锁

​ 把CPU和主内存之间的通信锁住,锁定期间,其他处理器无法访问其他内存地址的数据。开销很大,显然不合适

(2)缓存锁

​ 基于缓存一致性协议(MESI)实现,控制所得粒度。

MESI协议

a),M(Modify):共享数据被修改了为Modify,也就是缓存的数据和主内存不一致

b),E(Exclusive):表示缓存的独占状态,数据只缓存在当前缓存,其他处理器没有。

c),S(Shared):数据被多个CPU缓存,且各个缓存与主内存数据一致

d),I(Invalid):表示缓存已经失效

​ 在MESI协议中,每个缓存的缓存控制器不仅知道自己的读写操作,而且也监听其它Cache的读写操作。

​ 对于MESI协议,从CPU读写角度来说会遵循以下原则:

​ (1)CPU读请求:缓存处于M,E,S状态的都可以被读取,I状态CPU只能从主存中读取数据

​ (2)CPU写请求:缓存处于M,E状态才可以被写,对于S状态的写,需要将其他CPU中缓存行置为无效才可写。

MESI优化带来的可见性问题

​ MESI的优化可能会导致CPU的乱序执行,这种乱序执行会带来可见性的问题。(假如当前修改的是CPU0,其他CPU线程简称为CPU1)过程就是CPU0引入了storebuff,将数据的修改执行放到storebuff,然后发送消息给CPU1,这时候CPU0可以继续执行接下来的代码,当storebuff收到CPU1线程的ack应答消息后,storebuff将修改的数据同步到缓存行,再同步到主内存当中。

CPU层面的内存屏障

​ 内存屏障就是将storebuffer中的指令写入主内存,从而使得其他访问同一共享内存的线程的可见性。

a),读屏障:可以看做是一定将数据从高速缓存中抹掉,从内存中读出来。保证读操作有序。

b),写屏障:可以看做是一定将数据写回内存,而不是写到高速缓存中。写屏障之前的指令的结果对屏障之后的读或者写是可见的。保证写操作有序

c),通用屏障:保证读写操作有序。

所有的CPU内存屏障(除了数据依赖屏障外)都隐含了编译器屏障(也就是使用CPU内存屏障后就无需再额外添加编译器屏障了)

操作系统增加了进程、线程。通过CPU时间片的切换最大提升CPU利用率

编译器指令优化,更合理的去利用CPU高速缓存

JMM

定义

​ java内存模型,Java Memory Model,由前面分析得知,缓存及重排序导致了可见性的问题,JMM定义了共享内存中多线程程序读写操作的行为规范,通过这些规则来规范对内存的读写操作从而保证指令的正确性。它解决了CPU多级缓存、处理器优化、指令重排序导致的内存访问问题,保证了并发场景下的可见性问题。

如何解决可见性有序性问题

​ 简单来说,它提供了一些禁用缓存及禁止重排序的方法,如volatile,synchronized,final等

重排序问题

​ 为了提高程序的运行性能,编译器和处理器都会对指令做重排序。

​ 源代码 --> 1.编译器重排序 --> 2.指令重排序 --> 3.内存系统重排序 --> 最终执行的指令序列

​ 1,编译器的重排序,编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序。JMM提供了禁止特定类型的编译器重排序

​ 2,处理器重排序(2和3),如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。JMM会要求编译器生成指令时,会插入内存屏障来禁止处理器重排序。

JMM层面的内存屏障

屏障类型 指令示例 说明
LoadLoad Barriers Load1; LoadLoad; Load2 确保Load1数据的装载,之前于Load2及所有后续装载指令的装载
StoreStore Barriers Store1; StoreStore; Store2 确保Store1数据对于其他处理器可见(刷新到内存),之前于Store2及所有后续存储指令的存储
LoadStore Barriers Load1; LoadStore; Store2 确保Load1数据的装载,之前于Store2及所有后续的存储指令刷新到内存
StoreLoad Barriers Store1; StoreLoad; Load2 确保Store1数据对于其他处理器可见,之前于Load2及所有后续装载指令的装载,StoreLoad Barriesrs会使该屏障之前的所有内存访问指令(存储和装载指令)完成之后,才执行该屏障之后的内存访问指令。

HappenBefore

​ 用happens-before的概念来阐述操作之间的内存可见性。在JMM中,如果一个操作执行的结果需要对另一个操作可见,那么这两个操作之间必须要存在happens-before关系 。happens-before关系本质上和as-if-serial语义是一回事。as-if-serial语义保证单线程内程序的执行结果不被改变,happens-before关系保证正确同步的多线程程序的执行结果不被改变。

  • 程序顺序规则:一个线程中的每个操作,happens-before于该线程中的任意后续操作
  • 监视器锁规则:对一个锁的解锁,happens-before于随后对这个锁的加锁
  • volatile变量规则:对一个volatile域的写,happens-before于任意后续对这个volatile域的读。
  • 传递性:如果A happens-before B,且B happens-before C,那么A happens-before C
  • start()规则:如果线程A执行操作ThreadB.start()(启动线程B),那么A线程的ThreadB.start()操作happens-before于线程B中的任意操作。
  • join规则:如果线程A执行操作ThreadB.join()并成功返回,那么线程B中的任意操作happens-before于线程A从ThreadB.join()操作成功返回。
  • 线程中断规则:对线程interrupt方法的调用happens-before于被中断线程的代码检测到中断事件的发生。
posted @ 2020-04-21 15:16  gnice512  阅读(547)  评论(1编辑  收藏  举报