ICode9

精准搜索请尝试: 精确搜索
首页 > 其他分享> 文章详细

线程安全性之可见性、缓存一致性(MESI)以及伪共享问题分析

2021-12-04 15:02:23  阅读:199  来源: 互联网

标签:缓存 变量 CPU1 修改 MESI 线程 共享 CPU


可见性问题

可见性是什么:线程A变量对线程B不可见,例如数据库脏读。

1.代码示例

    static boolean flag = false;
    static int num = 0;
    public static void main(String[] args) throws InterruptedException {
        new Thread(()->{
            //里面无触发活性的东西 会导致活性失效
            while (!flag){
               num++;
            }
        }).start();
        Thread.sleep(1000);
        System.out.println(num);
        flag = true;
    }
	//输出结果
	1255362997

然后惊奇的发现,程序并没有停止呀,可见性问题就此展开

2.活性失效

简单来说,程序进行的值在没有触发操作,没有进行重新加载,也就还是以前的值。

//然后在里面加入这个 
System.out.println(num);
//或者
try {
    Thread.sleep(10);
} catch (InterruptedException e) {
    e.printStackTrace();
}

加上上述代码,程序又能正常结束了,这是为什么呢?

sout 是IO里面的操作,里面有synchronized同步操作

public void println(int x) {
    synchronized (this) {
        print(x);
        newLine();
    }
}

Thread.sleep(10)是阻塞操作,会继续持有锁,但是放弃cpu执行机会,导致上下文切换

综上所述:上面两种方式都会重新加载值,故而可以正常结束线程

CPU高速缓存

1.由来

磁盘(程序) -> 加载程序 -> 内存(数据) -> 运行 -> CPU

CPU的运行速度会比磁盘读写快的多,那么在磁盘读写时,CPU将处于阻塞状态,造成了CPU资源的浪费,于是便有了一系列的优化

  • CPU资源利用问题

CPU增加3级高速缓存(L1 L2 L3)
操作系统中,增加进程、线程 ->通过CPU的时间片切换,提升CPU利用率
编译器优化(JVM的深度优化)

2.缓存一致性

在我们的任务管理器 -CPU界面可以看到 L1 L2 L3缓存
CPU - L1(区分) - L2(单个CPU共享) - L3(整个共享) - 主存,顺序为依次读取,但是又有了新的问题
缓存一致性问题
从上图可以看到,CPU1在修改完flag后同步到主存,但是当CPU2去取flag的时候,因为先从缓存取的缘故,可能还有旧的值,这就是缓存一致性问题。

3.缓存一致性解决方案

毋庸置疑:加锁

  • 总线锁,更新到主存处加锁

  • 缓存锁

    • 缓存一致性协议(MESI,MOSI)
    • MESI,分别表示修改(modify)、独占(exclusive)、共享(share)、失效(invalid)

    修改:当CPU去修改i变量的时候,会把状态修改为modify状态
    独占:该变量只存在当前CPU缓存行中
    共享:多个CPU都加载了该变量
    失效:当缓存行中i变量发生改变时,发现是共享状态,那么需要通知另一个CPU的i变量修改为失效状态
    根据MESI协议,读取的时候只有MES走缓存,I状态下的要直接访问内存

  • 看下MESI协议流程图
    缓存MESI协议
    当修改i = 1时,整个流程如下

    • CPU1修改变量i时,状态会修改为M(修改)状态,同时通知CPU2的变量i修改为I(无效)状态
    • CPU1修改完成后同步到主存,并将变量i设置为E(独占)状态
    • 当CPU2操作变量i时,发现是失效状态,会去内存中重新加载。最终通过总线探测得到CPU1也有加载变量i,就会将CPU1和CPU2中的变量i都会修改为S(共享)状态,否则就是独占状态

总得来说就是在修改高速缓存共享(share)状态下的时候,会发送一个失效指令给其他CPU,然后其他CPU读取高速缓存时发现是失效状态,那么从重新从内存加载进来,以解决缓存一致性问题。
看下解决方案图:
高速缓存加锁
从上图可以看图更新缓存会走到缓存锁/总线锁,具体实现不是我们实现,而是由CPU加上汇编指令#Lock去实现,当我们加上volatile关键字后,最终的执行指令中就会生成#Lock汇编指令,从而达到加锁的效果

总结: 在java中加上 volatile关键字,就等于加上了汇编指令#Lock,达到加锁的效果

  • 查看运行的汇编指令可以参考hsdis,这里不贴图了,点击去百度
  • 具体区别就是加了Volatile关键字和没加的汇编指令中有没有#Lock的区别

注意点:MESI协议只能保证并发编程中的可见性,并未解决原子性和有序性的问题,所以只靠MESI协议是无法完全解决多线程中的所有问题。

伪共享问题

1.伪共享问题的表现

并发修改在一个缓存行中的多个独立变量,表面上是并发执行的,但实际在CPU处理的时候是串行执行的,并发的性能有很大的影响。

缓存是由缓存行组成的,通常是64字节组成。
一个java的long类型是8字节,因此一个缓存行中可以存放8个long类型的变量。

缓存行是CPU内部用来存放数据的最小存储区域,缓存行每次加载数据是一段一段的(提升性能),X86的电脑一段就是64位,但在这个缓存行下会出现以下问题
伪共享问题
一个缓存行中X、Y、Z三个变量,那么在CPU0操作X时,那么在CPU1中,整个缓存行就失效了,这个时候,如果CPU1修改了Y的值,就必须先提交CPU1的缓存,然后再去主存中读取数据,这样就出现了问题,XY在两个CPU上被修改,本是一个并行的操作,但由于缓存一致性,却成为了串行,会严重影响并发的性能,这就叫做伪共享问题。

2.伪共享问题的解决方案

引入对齐填充,不足64位的情况下,采取补齐
Java中提供了两种方案:

  • 填充法:在两个long类型中间使用额外的7个long进行填充
public class Pointer{
    long index;
    long a1,a2,a3,a4,a5,a6,a7;
    long count;
}

上面的代码使用填充法后,在内存行中的布局如下

index
count

  • 使用@Contentded注解
    对类使用时:是整个字段快两端都被填充
    对字段使用时:该字段会和其他字段分离到不存的缓存行上,同时还支持contention group属性,同一组的字段在内存上是连续的。

@sun.misc.Contended(“v1”)

该注解需要添加JVM启动参数才能生效:-XX:-RestrictContended

以上就是本章的全部内容了。

上一篇:并发编程之锁的认识和同步锁 – synchronized
下一篇:线程安全性之有序性和内存屏障

野火烧不尽,春风吹又生

标签:缓存,变量,CPU1,修改,MESI,线程,共享,CPU
来源: https://blog.csdn.net/qq_35551875/article/details/121651039

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有