当前位置: 首页 > article >正文

Java并发编程之可见性、原子性和有序性

引言

CPU缓存与内存产生的一致性问题(可见性)
CPU时间片切换产生的原子性问题
CPU指令编译优化产生的有序性问题

并发编程问题的根源

CPU、内存、I/O设备三者速度差异一直是 核心矛盾 三者速度差异可形象描述为:天上一天(CPU),地上一年(内存),地下十年(I/O) 根据木桶理论,程序整体性能取决于最慢的操作-读写I/O设备,可见单方面提高CPU性能是无效的。

为了合理利用CPU的高性能,平衡三者的速度差异,计算机体系结构、操作系统、编译程序都做了努力:

  1. CPU增加了缓存,以均衡与内存的速度差异
  2. 操作系统增加了进程、线程,以及分时复用CPU,进而均衡CPU与I/O设备的速度差异
  3. 编译程序优化指令执行次序,使得缓存能够得到更加合理的利用

1.1 源头之一:缓存导致的可见性问题

什么是可见性:一个线程对共享变量的修改,另外一个线程能够立刻看到。

但是多核时代,每颗 CPU 都有自己的缓存, CPU 缓存与内存的数据一致性就没那么容易解决了
在这里插入图片描述

    private int count = 0;

    @Test
    public void testDemoAdd() throws InterruptedException {

        Thread thread01 = new Thread(this::add);
        Thread thread02 = new Thread(this::add);
        thread01.start();
        thread02.start();
        thread01.join();
        thread02.join();

        System.out.println("count = " + count);
    }

    public void add(){
        for (int i = 0; i < 10000; i++) {
            count+=1;
        }
    }

1.2 源头之二:线程切换带来的原子性问题

CPU时间片
在这里插入图片描述
示例:count += 1,至少需要三条 CPU 指令

指令 1:首先,需要把变量 count 从内存加载到 CPU 的寄存器;
指令 2:之后,在寄存器中执行 +1 操作;
指令 3:最后,将结果写入内存(缓存机制导致可能写入的是 CPU 缓存而不是内存)。

带来可能问题
在这里插入图片描述
原子性:一个或者多个操作在 CPU 执行的过程中不被中断的特性 CPU 能保证的原子操作是 CPU 指令级别的,而不是高级语言的操作符

1.3 源头之三:编译优化带来的有序性问题

有序性指的是程序按照代码的先后顺序执行

示例:利用双重检查创建单例对象

    // volatile 存在是保证内存的可见性,禁止指令重排序
    // 原因:在Java指令中创建对象和赋值操作是分开进行的,也就是说instance = new Singleton();语句是分两步执行的。
    // 但是JVM并不保证这两个操作的先后顺序,也就是说有可能JVM会为新的Singleton实例分配空间,然后直接赋值给instance成员,
    // 然后再去初始化这个Singleton实例

    // 举例: 重排序会 导致 step2 和 step3 执行的顺序颠倒
    // 创建对象的步骤:step1:在内存中分配一块空间。step2:对内存空间进行初始化。step3:把对象在内存中的位置指向 instance。
    // 现在假设有两个线程T1、T2,T1 线程执行完重排序后的 step3 ,CPU 的执行权被 T2 获得。这个时候,instance 已经不为 null 了,
    // 他指向了内存中的一块地址。T2 执行到第一个 if 的时候,发现 instance 不为 null,就直接返回,但是这个 instance 并没有被初始化,
    // 这就会导致 T2 在执行的过程中发生不可预知的错误。
    private volatile static DoubleCheckSingleton singleton;

    private DoubleCheckSingleton() {
    }

    public static DoubleCheckSingleton getInstance() {
        // 检查是否已经被创建——第一个 if 可以避免频繁加锁,如果没有第一个 if,它就会直接尝试获取锁资源
        if (singleton == null) {
            // 同步块
            synchronized (DoubleCheckSingleton.class) {
                // 再次检测是否被创建----双重检测

                // 第二个if是避免重复创建线程,破坏单例

                // 现假设有两个 T1 和 T2,T1 执行到同步块时,CPU 的执行权被 T2 抢夺走,T2 执行完成之后创建了一个对象实例,
                // 并且释放 Java 的类锁。这个时候 T1 又重新获得了 CPU 的执行权,并且获得了类锁。
                // 如果没有第二个 if 的判断,T1 又会重新创建一个 实例对象,这样就破坏了单例。
                if (singleton == null) {
                    // 标记3
                    singleton = new DoubleCheckSingleton();
                }
            }
        }
        return singleton;
    }

new的理论顺序:

  1. 分配一块内存M
  2. 在内存M上初始化Singleton对象
  3. M的地址赋值给instance变量

经过编译器实际优化后:分配一块内存M,带来问题:
在这里插入图片描述

总结

可见性是一个线程对共享变量的修改,另一个线程能够立刻看到,如果不能立刻看到,就可能会产生可见性问题。在单核CPU上是不存在可见性问题的,可见性问题主要存在于运行在多核CPU上的并发程序。归根结底,可见性问题还是由CPU的缓存导致的,而缓存导致的可见性问题是导致诸多诡异的并发编程问题的“幕后黑手”之一。我们只有深入理解了缓存导致的可见性问题,并在实际工作中时刻注意避免可见性问题,才能更好的编写出高并发程序。


http://www.kler.cn/a/567487.html

相关文章:

  • C语言-7.函数
  • 6-1JVM的执行引擎处理
  • CF 109A.Lucky Sum of Digits(Java实现)
  • ffmpeg-static 依赖详解
  • 芯麦GC1277与0CH477驱动芯片对比分析:电脑散热风扇应用的性能优势与替代方案
  • 在线抽奖系统——管理员注册
  • 张量运算全解析
  • NO.22十六届蓝桥杯备战|一维数组|七道练习|冒泡排序(C++)
  • 量子计算如何提升机器学习效率:从理论到实践
  • 蓝桥杯2024年第十五届省赛真题-传送阵
  • Vue3+Vite开发Electron桌面端问题记录
  • 快速排序(c++)
  • 深入理解并实现自定义 unordered_map 和 unordered_set
  • 异或和之和 | 前缀+位运算+奉献
  • [特殊字符] 深度探索推理新境界:DeepSeek-R1如何用“自学”让AI更聪明? [特殊字符]
  • 分享---rpc运维事故处理
  • 使用Kotlin实现动态代理池的多线程爬虫
  • 汽车智能感应钥匙PKE低频天线的作用
  • mysql中的的锁
  • 象棋笔记-实战记录