栏目分类:
子分类:
返回
终身学习网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
终身学习网 > IT > 软件开发 > 后端开发 > Java

并发编程 - 可见性有序性

Java 更新时间:发布时间: 百科书网 趣学号

        可见性有序性代码示例如下:

public class VolatileDemo {

    public static boolean stop = false;

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(()->{
            int i = 0;
            while (!stop){
                i++;
            }
        });
        t1.start();
        System.out.println("start thread");
        Thread.sleep(1000);
        stop = true;
    }
}

        我们期望的执行结果是 t1 线程能正常执行结束。但是结果是一直等不到结束。也就是说在 main 线程修改的 stop 的值,对于线程 t1 不可见。

一、volatile 解决可见性问题

        我们在代码中给 石头stop 用 volatile 修饰。结果就和我们预期的一样,修改 stop 的值后,线程 t1 拿到了值,并且正常结束了。

public class VolatileDemo {

    public volatile static boolean stop = false;

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(()->{
            int i = 0;
            while (!stop){
                i++;
            }
        });
        t1.start();
        System.out.println("start thread");
        Thread.sleep(1000);
        stop = true;
    }
}
二、可见性有序性的由来 1、为提升处理性能所做的优化

        在计算机的发展历史过程中,除了 CPU 、内存、IO设备 的不断迭代更新来提升性能,还有一个比较矛盾的地方,就是这三者在处理速度上的差异。

        CPU 的计算速度是最快的,其次是内存,最后是 IO 设备,也就是说 CPU 的计算速度远远高于内存以及磁盘,比如在一台2.4GHz的cpu上,每秒能处理2.4x109次,每次 处理的数据量,如果是64位操作系统,那么意味着每次能处理64位数据量。

        虽然 CPU 从单核到多核,甚至到超线程技术等最大化的提升了 CPU 的性能,但是仅仅提升 CPU 的性能是不够的,如果磁盘和内存的处理性能没有跟上,就意味着整体计算机效率取决于最慢的设备,为了平衡这三者的差异,最大化的利用 CPU 所以在硬件层面,操作系统层面,编译器层面都做了很多的优化。

        CPU 层面:CPU 增加了高速缓存。

        操作系统层面:增加了线程,进程,通过 CPU 时间片切换,最大化提升 CPU 的使用率。

        编译器指令优化:指令重排序,更加合理的利用 CPU 高速缓存。

        但是,每一种优化,都会带来线程安全问题,这就是线程安全问题的来源。

2、CPU 层面做的优化带来的问题(CPU 高速缓存) 

        我们打开电脑的任务管理器,点开性能这一栏,在下面就可以看到 L1 缓存,L2 缓存,L3 缓存,就是我们的 CPU 高速缓存。

         但是这三个高速缓存是什么关系呢?如下图

 3、CPU 高速缓存带来的缓存一致性问题

        CPU 高速缓存的出现,虽然提升了 CPU 的利用率,但是也带来了另外一个问题,缓存一致性问题。什么是缓存一致性问题呢?

        在多线程环境中,当多个线程执行加载同一块内存数据的时候,由于每个 CPU 都有自己独立的一块高速缓存 L1 L2 。所以每个 CPU 的这部分空间就会缓存到相同的数据。并且每个 CPU 执行相关指令时,对其它 CPU 都不可见,就会导致缓存一致性问题。如下图:

         如图所示,Processor 1 对 X 的值进行修改了之后,又同步到内存中,但是在 Processor0 中的缓存中的 x 的值还是 原来的值。

4、缓存一致性协议 MESI(缓存锁)

        为了达到缓存一致性,需要在各个 CPU 访问缓存的时候都遵守一些协议,在读写的时候,根据协议来操作,常见的协议有 MSI  MESI  MOSI  。最常用的就是 MESI 。

        MESI 其实是表示四种状态,分别是:

        M(modify):表示的是共享数据只保存在当前缓存中,并且是被修改状态,也就是缓存中的数据和主内存中的数据不一致。

        E(Exclusive):表示缓存独占状态,数据只是缓存在当前 CPU 缓存中,并且没有被修改。

        S(Shared):表示数据可能被多个 CPU 缓存,并且缓存中的数据和主内存中的数据一致。

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

        在 CPU 缓存中,每一个 Cache 一定会在 Shard Exclusive Invalid 三种状态之一

         如上图,我们在修改数据之前,会通过一种方式通知其它缓存行缓存的数据失效,等当前 CPU修改完成之后给写到内存中,再同步到其它 CPU 的过程。其实就是在缓存层面加一把锁。在汇编指令层面就是加了一个 #Lock 指令。

        我们的 volatile 关键字其实就是在汇编指令层面加了 #lock 指令。

 5、编译器指令优化 - 指令重排序
public class SeqExample {

    private volatile static int x = 0, y = 0;
    private volatile static int a = 0, b = 0;

    public static void main(String[] args) throws InterruptedException {
        int i = 0;
        for (; ; ) {
            i++;
            //伪共享代码
            x = 0;
            y = 0;
            a = 0;
            b = 0;
            Thread t1 = new Thread(() -> {
                a = 1;
                x = b;
                //x=b; a=1;
            });
            Thread t2 = new Thread(() -> {
                b = 1;
                y = a;
                //y=a; b=1;
            }); 
            t1.start();
            t2.start();
            t1.join();
            t2.join();
            String result = "第" + i + "次(" + x + "," + y + ")";
            if (x == 0 && y == 0) {
                System.out.println(result);
                break;
            } else {

            }
        }
    }
}

        x 和 y 的值 可能的结果是

                t1 先执行完成,x = 0,y = 1

                t2 先执行完成,x = 1 ,y = 0

                还有可能是  : x = 1,y = 1

                但是上面代码一直运行下去,会出现的结果是 x = 0 , y  = 0 

        说明线程执行的顺序:x = b , y = a ,a = 1 , b = 1 。所以结果为 :x = 0 , y  = 0 

        这样就出现了指令重排序。

(1)在 CPU 层面是如何导致重排序的?

        在CPU 层面引入了 Store Buffer 实际上是一种异步的思想。

        

          Store Buffer 用来干啥?来看下面一张图:

       

         其实在 CPU 0 执行写操作,需要通知其他 CPU 让缓存行失效,只有收到其他 CPU 给的失效消息后,才能将数据同步到内存,这段时间是等待其他 CPU 响应,CPU 0 其实是处于阻塞状态,CPU 是宝贵的资源,不能浪费,Store Buffer 其实就是 在 CPU 和缓存之间加了一个异步的通知方法,CPU 只要先写入 Store Buffer 再同步到缓存行中,不需要阻塞,CPU 还可以做其他事情,由Store Buffer 异步的去让其他 CPU 的缓存行失效。这就是 Store Buffer 的作用。

        以后补齐。

6、内存屏障

        指令重排序,这种顺序一致性导致的可见性问题,在 CPU 层面无法被解决,原因是 CPU 只是一个运算工具,它只接受指令并且执行指令,并不清楚当前执行的整个逻辑是否存在不能优化的问题,也就是说硬件层面无法解决这种顺序一致性导致的可见性问题。

        上面的问题出现,于是在 CPU 层面提供了写屏障、读屏障、全屏障这样的指令,在x86架构中,这三种指令分别是 SFENCE、LFENCE、MFENCE指令。

        sfence:也就是save fence,写屏障指令。在sfence指令前的写操作必须在sfence指令后的写操作前完成。

        lfence:也就是load fence,读屏障指令。在lfence指令前的读操作必须在lfence指令后的读操作 前完成。

        mfence:也就是modify/mix,混合屏障指令,在mfence前得读写操作必须在mfence指令后的读 写操作前完成。

转载请注明:文章转载自 www.051e.com
本文地址:http://www.051e.com/it/275821.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 ©2023-2025 051e.com

ICP备案号:京ICP备12030808号