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

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

作者:互联网

可见性问题

可见性是什么:线程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增加3级高速缓存(L1 L2 L3)
操作系统中,增加进程、线程 ->通过CPU的时间片切换,提升CPU利用率
编译器优化(JVM的深度优化)

2.缓存一致性

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

3.缓存一致性解决方案

毋庸置疑:加锁

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

总结: 在java中加上 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中提供了两种方案:

public class Pointer{
    long index;
    long a1,a2,a3,a4,a5,a6,a7;
    long count;
}

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

index
count

@sun.misc.Contended(“v1”)

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

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

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

野火烧不尽,春风吹又生

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