zoukankan      html  css  js  c++  java
  • Java并发编程实战 01并发编程的Bug源头

    摘要

    编写正确的并发程序对我来说是一件极其困难的事情,由于知识不足,只知道synchronized这个修饰符进行同步。
    本文为学习极客时间:Java并发编程实战 01的总结,文章取图也是来自于该文章

    并发Bug源头

    在计算机系统中,程序的执行速度为:CPU > 内存 > I/O设备 ,为了平衡这三者的速度差异,计算机体系机构、操作系统、编译程序都进行了优化:

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

    但是这三者导致的问题为:可见性、原子性、有序性

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

    一个线程对共享变量的修改,另外一个线程能够立即看到,那么就称为可见性。
    现在多核CPU时代中,每颗CPU都有自己的缓存,CPU之间并不会共享缓存;

    如线程A从内存读取变量V到CPU-1,操作完成后保存在CPU-1缓存中,还未写到内存中。
    此时线程B从内存读取变量V到CPU-2中,而CPU-1缓存中的变量V对线程B是不可见的
    当线程A把更新后的变量V写到内存中时,线程B才可以从内存中读取到最新变量V的值

    上述过程就是线程A修改变量V后,对线程B不可见,那么就称为可见性问题。

    file

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

    现代的操作系统都是基于线程来调度的,现在提到的“任务切换”都是指“线程切换”
    Java并发程序都是基于多线程的,自然也会涉及到任务切换,在高级语言中,一条语句可能就需要多条CPU指令完成,例如在代码 count += 1 中,至少需要三条CPU指令。

    指令1:把变量 count 从内存加载到CPU的寄存器中
    指令2:在寄存器中把变量 count + 1
    指令3:把变量 count 写入到内存(缓存机制导致可能写入的是CPU缓存而不是内存)

    操作系统做任务切换,可以发生在任何一条CPU指令执行完,所以并不是高级语言中的一条语句,不要被 count += 1 这个操作蒙蔽了双眼。假设count = 0,线程A执行完 指令1 后 ,做任务切换到线程B执行了 指令1、指令2、指令3后,再做任务切换回线程A。我们会发现虽然两个线程都执行了 count += 1 操作。但是得到的结果并不是2,而是1。

    file

    如果 count += 1 是一个不可分割的整体,线程的切换可以发生在 count += 1 之前或之后,但是不会发生在中间,就像个原子一样。我们把一个或者多个操作在 CPU 执行的过程中不被中断的特性称为原子性

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

    有序性指的是程序按照代码的先后顺序执行。编译器为了优化性能,可能会改变程序中的语句执行先后顺序。如:a = 1; b = 2;,编译器可能会优化成:b = 2; a = 1。在这个例子中,编译器优化了程序的执行先后顺序,并不影响结果。但是有时候优化后会导致意想不到的Bug。
    在单例模式的双重检查创建单例对象中。如下代码:

    public class Singleton {
        private static Singleton instance;
        private Singleton() {}
        public static Singleton getInstance() {
            if (instance == null) {
                synchronized (Singleton.class) {
                    if (instance == null) {
                        instance = new Singleton();
                    }
                }
            }
            return instance;
        }
    }
    

    问题出现在了new Singletion()这行代码,我们以为的执行顺序应该是这样的:

    指令1:分配一块内存M
    指令2:在内存M中实例化Singleton对象
    指令3:instance变量指向内存地址M

    但是实际优化后的执行路径确实这样的:

    指令1:分配一块内存M
    指令2:instance变量指向内存地址M
    指令3:在内存M中实例化Singleton对象

    这样的话看出来什么问题了吗?当线程A执行完了指令2后,切换到了线程B,
    线程B判断到 if (instance != null)。直接返回instance,但是此时的instance还是没有被实例化的啊!所以这时候我们使用instance可能就会触发空指针异常了。如图:

    file

    总结

    在写并发程序的时候,需要时刻注意可见性、原子性、有序性的问题。在深刻理解这三个问题后,写起并发程序也会少一点Bug啦~。记住了下面这段话:CPU缓存会带来可见性问题、线程切换带来的原子性问题、编译优化带来的有序性问题。

    参考文章:极客时间:Java并发编程实战 01 | 可见性、原子性和有序性问题:并发编程Bug的源头

    个人博客网址: https://colablog.cn/

    如果我的文章帮助到您,可以关注我的微信公众号,第一时间分享文章给您
    微信公众号

  • 相关阅读:
    Roce ofed 环境搭建与测试
    Ubuntu 1804 搭建NFS服务器
    Redhat 8.0.0 安装与网络配置
    Centos 8.1 安装与网络配置
    SUSE 15.1 系统安装
    VSpare ESXi 7.0 基本使用(模板、iso、SRIOV)
    VSpare ESXi 7.0 服务器安装
    open SUSE leap 15.1 安装图解
    KVM虚拟机网卡连接网桥
    GitHub Action一键部署配置,值得拥有
  • 原文地址:https://www.cnblogs.com/Johnson-lin/p/12697533.html
Copyright © 2011-2022 走看看