从volatile关键字到总线风暴

volatile关键字

一、计算机内存模型

​ 计算机执行是指计算机内一系列指令在cpu中执行的过程,在指令执行过程中会涉及到内存中数据的读写,这时如果直接进行读写相对于cpu指令执行而言是非常耗费时间的。针对这种情况计算机会在cpu中开辟一片区域(高速缓存),通过高速缓存进行数据的读取就会快很多。但是在多线程中这种情况就会存在问题,多个线程之间的高速缓存数据不可见,与内存中数据不一致等情况。这时计算机通过缓存一致性行协议与主线lock锁方式来保证高速缓存一致性问题。

二、java中并发编程的三个概念:

1.原子性

​ 原子性:原子性是指不可再分的操作,对于单线程而言其是计算机的一个指令。对于多线程而言其可以是一个指令,也可以是加锁的指令片段,必须保证其操作全部执行或者全部不执行。对于以下代码而言只有语句1是原子性操作,其它都不是。简单来说就是简单的读取和赋值(只能是将数字赋值给变量,变量之间赋值不是)是单指令操作。

x=1;       //1 原子操作,给x赋值1
y=x;	   //2 非原子操作,取x的值,赋给y
y++;	   //3 取y的值,加1,赋给y
y=y+1;	   //4 类似与3

2.可见性

​ 对以下代码而言,线程1将0赋值个a但是并没有刷入到内存中,这时变量a对线程b既是不可见的。这时将赋值给变量b就会存在问题。java中使用volatile、synchronized和Lock来保证可见性。

//线程1
int a = 0;
a = 10;
 
//线程2
b = a;

3.有序性

​ java中运行计算机对指令进行重排序,重排序保证单个线程中具有依赖关系的两条指令顺序不变,但是不保证不存在依赖关系的指令之间的顺序,即保证单线程执行结果一致。对于以下代码语句1和语句2之间存在数据依赖关系,不会进行重排序。但语句2和语句3之间不存在依赖关系,就可以进行重排序。

int x=1;  //语句1
int y=x;  //语句2
int z=3;  //语句3

三、指令重排序

​ java为了指令优化允许指令进行重排序,但重排序要保证单线程执行结果的一致性。指令之间存在数据依赖关系的指令不能进行指令重排序。

一旦一个共享变量(类的成员变量、类的静态成员变量)被volatile修饰之后,那么就具备了两层语义:

1)保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。

2)禁止进行指令重排序。

但是用volatile修饰之后就变得不一样了:

第一:使用volatile关键字会强制将修改的值立即写入主存;

第二:使用volatile关键字的话,当线程2进行修改时,会导致线程1的工作内存中缓存变量stop的缓存行无效(反映到硬件层的话,就是CPU的L1或者L2缓存中对应的缓存行无效);

第三:由于线程1的工作内存中缓存变量stop的缓存行无效,所以线程1再次读取变量stop的值时会去主存读取。

volatile关键字禁止指令重排序

1)当程序执行到volatile变量的读操作或者写操作时,在其前面的操作的更改肯定全部已经进行,且结果已经对后面的操作可见;在其后面的操作肯定还没有进行;

2)在进行指令优化时,不能将在对volatile变量访问的语句放在其后面执行,也不能把volatile变量后面的语句放到其前面执行。

四、内存屏障

​ 观察加入volatile关键字和没有加入volatile关键字时所生成的汇编代码发现,加入volatile关键字时,会多出一个lock前缀指令

lock前缀指令实际上相当于一个内存屏障(也成内存栅栏),内存屏障会提供3个功能:

1)它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成;

2)它会强制将对缓存的修改操作立即写入主存;

3)如果是写操作,它会导致其他CPU中对应的缓存行无效。

五、总线风暴

​ 在java中使用unsafe实现cas,而其底层由cpp调用汇编指令实现的,如果是多核cpu是使用lock cmpxchg指令,单核cpu 使用compxch指令。如果在短时间内产生大量的cas操作在加上 volatile的嗅探机制则会不断地占用总线带宽,导致总线流量激增,就会产生总线风暴。 总之,就是因为volatile 和CAS 的操作导致BUS总线缓存一致性流量激增所造成的影响。

在这里插入图片描述

1、总线锁

在早期处理器提供一个 LOCK# 信号,CPU1在操作共享变量的时候会预先对总线加锁,此时CPU2就不能通过总线来读取内存中的数据了,但这无疑会大大降低CPU的执行效率。

2、缓存一致性协议

由于总线锁的效率太低所以就出现了缓存一致性协议,Intel 的MESI协议就是其中一个佼佼者。MESI协议保证了每个缓存变量中使用的共享变量的副本都是一致的。

3、MESI 的核心思想

modified(修改)、exclusive(互斥)、share(共享)、invalid(无效)

​ 如上图,CPU1使用共享数据时会先数据拷贝到CPU1缓存中,然后置为独占状态(E),这时CPU2也使用了共享数据,也会拷贝也到CPU2缓存中。通过总线嗅探机制,当该CPU1监听总线中其他CPU对内存进行操作,此时共享变量在CPU1和CPU2两个缓存中的状态会被标记为共享状态(S);

​ 若CPU1将变量通过缓存回写到主存中,需要先锁住缓存行,此时状态切换为(M),向总线发消息告诉其他在嗅探的CPU该变量已经被CPU1改变并回写到主存中。接收到消息的其他CPU会将共享变量状态从(S)改成无效状态(I),缓存行失效。若其他CPU需要再次操作共享变量则需要重新从内存读取。

缓存一致性协议失效的情况:

  • 共享变量大于缓存行大小,MESI无法进行缓存行加锁;
  • CPU并不支持缓存一致性协议

4、嗅探机制

每个处理器会通过嗅探器来监控总线上的数据来检查自己缓存内的数据是否过期,如果发现自己缓存行对应的地址被修改了,就会将此缓存行置为无效。当处理器对此数据进行操作时,就会重新从主内存中读取数据到缓存行。

5、缓存一致性流量

通过前面都知道了缓存一致性协议,比如MESI会触发嗅探器进行数据传播。当有大量的volatile 和cas 进行数据修改的时候就会产大量嗅探消息。

六、使用情况

​ synchronized关键字是防止多个线程同时执行一段代码,那么就会很影响程序执行效率,而volatile关键字在某些情况下性能要优于synchronized,但是要注意volatile关键字是无法替代synchronized关键字的,因为volatile关键字无法保证操作的原子性。通常来说,使用volatile必须具备以下2个条件:

1)对变量的写操作不依赖于当前值

2)该变量没有包含在具有其他变量的不变式中

实际上,这些条件表明,可以被写入 volatile 变量的这些有效值独立于任何程序的状态,包括变量的当前状态。

事实上,我的理解就是上面的2个条件需要保证操作是原子性操作,才能保证使用volatile关键字的程序在并发时能够正确执行。

摘自:https://www.cnblogs.com/dolphin0520/p/3920373.html

​ https://www.cnblogs.com/jiagoujishu/p/13744544.html

已标记关键词 清除标记
©️2020 CSDN 皮肤主题: 游动-白 设计师:上身试试 返回首页