专栏首页猿小俊Java并发编程实战 01并发编程的Bug源头

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不可见,那么就称为可见性问题。

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

现代的操作系统都是基于线程来调度的,现在提到的“任务切换”都是指“线程切换” 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。

如果 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可能就会触发空指针异常了。如图:

总结

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

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

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

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

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • Java并发编程实战 05等待-通知机制和活跃性问题

    Java并发编程实战 01并发编程的Bug源头 Java并发编程实战 02Java如何解决可见性和有序性问题 Java并发编程实战 03互斥锁 解决原子性问...

    Johnson木木
  • ThinkPHP5.1 + tufanbarisyildirim 解析apk

    对于apk,我可以说只会安装,并不知道其中有什么内容需要记录下来。这次公司做一个关于电视机顶盒的项目。对于这个陌生的项目,刚开始真是一脸懵逼,完全不知道如何下手...

    Johnson木木
  • ubuntu防火墙规则之ufw

    因公司项目的需求,需要对客户端机器简便使用防火墙的功能,所以可在页面进行简便设置防护墙规则,当然,这个功能需求放到我手上我才有机会学到。因为客户端机器都是ubu...

    Johnson木木
  • 并发面试必备系列之并发基础与内存模型

    在 《Awesome Interviews》 归纳的常见面试题中,无论前后端,并发与异步的相关知识都是面试的中重中之重,本系列即对于面试中常见的并发知识再进行回...

    王下邀月熊
  • 并发Bug之源有三,请睁大眼睛看清它们

    一切设计来源于生活,上一章 学并发编程,透彻理解这三个核心是关键 中有讲过,作为"资本家",你要尽可能的榨取 CPU,内存与 IO 的剩余价值,但三者完成任务的...

    用户4172423
  • Java 多线程(3)---- 线程的同步(上)

    我们在前面两篇文章中分别看了一下 Java 线程的一些概念、用法和对于线程控制(开始、暂停、停止)等,并对其中的一些易错点进行了总结,如果你是对这些概念还是还不...

    指点
  • 什么是Java内存模型

    有一个关于JVM名词定义的问题,说”JVM内存模型“,有人会说是关于JVM内存分布(堆栈,方法区等)这些介绍,也有地方说(深入理解JVM虚拟机)上说Java内存...

    java架构师
  • 支付宝如何优化移动端深度学习引擎?

    由于移动端资源的限制,大部分深度学习引擎都部署在云端,移动设备获取到输入数据,经过简单的加工,发送给云端,云端服务器经过深度神经网络推断运算,得到结果并反馈给移...

    刀刀老高
  • 分支预测

    分支预测( Branch predictor):当处理一个分支指令时,有可能会产生跳转,从而打断流水线指令的处理,因为处理器无法确定该指令的下一条指令,直到分支...

    233333
  • 什么?一个核同时执行两个线程?

    Hi,好久不见,我是CPU一号车间的阿Q,不认识我的话,可以看看:完了!CPU一味求快出事儿了!

    轩辕之风

扫码关注云+社区

领取腾讯云代金券