摘要

编写正确的并发程序对我来说是一件极其困难的事情,由于知识不足,只知道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/

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

Java并发编程实战 01并发编程的Bug源头的更多相关文章

  1. Java并发编程实战笔记—— 并发编程1

    1.如何创建并运行java线程 创建一个线程可以继承java的Thread类,或者实现Runnabe接口. public class thread { static class MyThread1 e ...

  2. Java并发编程实战笔记—— 并发编程2

    1.ThreadLocal Java中的ThreadLocal类可以让你创建的变量只被同一个线程进行读和写操作.因此,尽管有两个线程同时执行一段相同的代码,而且这段代码又有一个指向同一个ThreadL ...

  3. Java并发编程实战笔记—— 并发编程4

    1.同步容器类 同步容器类都是线程安全的,但在某些情况下可能需要额外的客户端加锁保护复合操作. 容器上常见的复合操作包括但不限于:迭代(反复访问数据,直到遍历完容器中所有的元素为止).跳转(根据指定顺 ...

  4. Java并发编程实战笔记—— 并发编程3

    1.实例封闭 class personset{ private final Set<Person> myset = new HashSet<Person>(); public ...

  5. Java并发编程实战 02Java如何解决可见性和有序性问题

    摘要 在上一篇文章当中,讲到了CPU缓存导致可见性.线程切换导致了原子性.编译优化导致了有序性问题.那么这篇文章就先解决其中的可见性和有序性问题,引出了今天的主角:Java内存模型(面试并发的时候会经 ...

  6. Java并发编程实战 03互斥锁 解决原子性问题

    文章系列 Java并发编程实战 01并发编程的Bug源头 Java并发编程实战 02Java如何解决可见性和有序性问题 摘要 在上一篇文章02Java如何解决可见性和有序性问题当中,我们解决了可见性和 ...

  7. Java并发编程实战 04死锁了怎么办?

    Java并发编程文章系列 Java并发编程实战 01并发编程的Bug源头 Java并发编程实战 02Java如何解决可见性和有序性问题 Java并发编程实战 03互斥锁 解决原子性问题 前提 在第三篇 ...

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

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

  9. Java并发编程实战——读后感

    未完待续. 阅读帮助 本文运用<如何阅读一本书>的学习方法进行学习. P15 表示对于书的第15页. Java并发编程实战简称为并发书或者该书之类的. 熟能生巧,不断地去理解,就像欣赏一部 ...

随机推荐

  1. 实操教程丨使用Pod安全策略强化K8S安全

    本文来自Rancher Labs 什么是Pod安全策略? Kubernetes Pod安全策略(PSP)是Kubernetes安全版块中极为重要的组件.Pod安全策略是集群级别的资源,用于控制Pod安 ...

  2. C语言实现简单计算器小项目

    昨天刚安装上devc++,半夜想着练练C语言吧 于是就看到实验楼有一个计算器的项目 之前做过一次,这次写的主要是思路 首先我们先从原理思考jia,实现简单的计算器就要具备加减乘除这些,看普通的计算器也 ...

  3. Linux内核文档:如何写符合 kernel-doc 规范的注释

    简介 Linux内核使用 Sphinx 实现把 Documentation 目录下的 reStructuredText 文件转换为非常漂亮的文档.文档既可以通过 make htmldocs 转换成 H ...

  4. onOK Modal.warning iview 要写一个函数 套上,不然会得不到异步调用,直接弹出的时候就执行了

    export const warning = (str, callback = _ => {}, outCallback = () => {}) => { Modal.warning ...

  5. ESLint {} 内部强制有空格 .eslintrc.js 配置文件 'object-curly-spacing': ["error", "always"],

    module.exports = { root: true, 'extends': [ 'plugin:vue/essential', '@vue/standard' ], rules: { 'obj ...

  6. 【Weiss】【第03章】练习3.16:删除相同元素

    [练习3.16] 假设我们有一个基于数组的表A[0,1...N-1],并且我们想删除所有相同的元素. LastPosition初始值为N-1,但应该随着相同元素被删除而变得越来越小. 考虑图3-61中 ...

  7. Mybatis详解系列(一)--持久层框架解决了什么及如何使用Mybatis

    简介 Mybatis 是一个持久层框架,它对 JDBC 进行了高级封装,使我们的代码中不会出现任何的 JDBC 代码,另外,它还通过 xml 或注解的方式将 sql 从 DAO/Repository ...

  8. Hinton老爷子CapsNet再升级,结合无监督,接近当前最佳效果

    2017 年,Geoffrey Hinton 在论文<Dynamic Routing Between Capsules>中提出 CapsNet 引起了极大的关注,同时也提供了一个全新的研究 ...

  9. 【NLP面试QA】基本策略

    目录 防止过拟合的方法 什么是梯度消失和梯度爆炸?如何解决? 在深度学习中,网络层数增多会伴随哪些问题,怎么解决? 关于模型参数 模型参数初始化的方法 模型参数初始化为 0.过大.过小会怎样? 为什么 ...

  10. 一、配置Ubuntu网络设置大纲

    root@ubuntu:为我的Ubuntu系统,即 用户名@主机名: 1.改主机名 ifconfig查询本机IP地址vim  /etc/hostname进入i编辑更改,改完按esc键 然后:wq!保存 ...