原创声明:本文转载自公众号【胖滚猪学编程】​

某日,胖滚猪写的代码导致了一个生产bug,奋战到凌晨三点依旧没有解决问题。胖滚熊一看,只用了一个volatile就解决了。并告知胖滚猪,这是并发编程导致的坑。这让胖滚猪坚定了要学好并发编程的决心。。于是,开始了我们并发编程的第一课。

序幕

BUG源头之一:可见性

刚刚我们说到,CPU缓存可以提高程序性能,但缓存也是造成BUG源头之一,因为缓存可以导致可见性问题。我们先来看一段代码:

private static int count = 0;
public static void main(String[] args) throws Exception {Thread th1 = new Thread(() -> {count = 10;});Thread th2 = new Thread(() -> {//极小概率会出现等于0的情况System.out.println("count=" + count);});th1.start();th2.start();
}

按理来说,应该正确返回10,但结果却有可能是0。

一个线程对变量的改变另一个线程没有get到,这就是可见性导致的bug。一个线程对共享变量的修改,另外一个线程能够立刻看到,我们称为可见性。

那么在谈论可见性问题之前,你必须了解下JAVA的内存模型,我绘制了一张图来描述:

主内存(Main Memory)

主内存可以简单理解为计算机当中的内存,但又不完全等同。主内存被所有的线程所共享,对于一个共享变量(比如静态变量,或是堆内存中的实例)来说,主内存当中存储了它的“本尊”。

工作内存(Working Memory)

工作内存可以简单理解为计算机当中的CPU高速缓存,但准确的说它是涵盖了缓存、写缓冲区、寄存器以及其他的硬件和编译器优化。每一个线程拥有自己的工作内存,对于一个共享变量来说,工作内存当中存储了它的“副本”。

线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主内存中的变量。
线程之间无法直接访问对方的工作内存中的变量,线程间变量的传递均需要通过主内存来完成

现在再回到刚刚的问题,为什么那段代码会导致可见性问题呢,根据内存模型来分析,我相信你会有答案了。当多个线程在不同的 CPU 上执行时,这些线程操作的是不同的 CPU 缓存。比如下图中,线程 A 操作的是 CPU-1 上的缓存,而线程 B 操作的是 CPU-2 上的缓存

由于线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主内存中的变量,那么对于共享变量V,它们首先是在自己的工作内存,之后再同步到主内存。可是并不会及时的刷到主存中,而是会有一定时间差。很明显,这个时候线程 A 对变量 V 的操作对于线程 B 而言就不具备可见性了 。

private volatile long count = 0;
​
private void add10K() {int idx = 0;while (idx++ < 10000) {count++;}
}
​
public static void main(String[] args) throws InterruptedException {TestVolatile2 test = new TestVolatile2();// 创建两个线程,执行 add() 操作Thread th1 = new Thread(()->{test.add10K();});Thread th2 = new Thread(()->{test.add10K();});// 启动两个线程th1.start();th2.start();// 等待两个线程执行结束th1.join();th2.join();// 介于1w-2w,即使加了volatile也达不到2wSystem.out.println(test.count);
}
​

原创声明:本文转载自公众号【胖滚猪学编程】​

原子性问题

一个不可分割的操作叫做原子性操作,它不会被线程调度机制打断的,这种操作一旦开始,就一直运行到结束,中间不会有任何线程切换。注意线程切换是重点!

我们都知道CPU资源的分配都是以线程为单位的,并且是分时调用,操作系统允许某个进程执行一小段时间,例如 50 毫秒,过了 50 毫秒操作系统就会重新选择一个进程来执行(我们称为“任务切换”),这个 50 毫秒称为“时间片”。而任务的切换大多数是在时间片段结束以后,

那么线程切换为什么会带来bug呢?因为操作系统做任务切换,可以发生在任何一条CPU 指令执行完!注意,是 CPU 指令,CPU 指令,CPU 指令,而不是高级语言里的一条语句。比如count++,在java里就是一句话,但高级语言里一条语句往往需要多条 CPU 指令完成。其实count++包含了三个CPU指令!

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

小技巧:可以写一个简单的count++程序,依次执行javac TestCount.java,javap -c -s TestCount.class得到汇编指令,验证下count++确实是分成了多条指令的。

volatile虽然能保证执行完及时把变量刷到主内存中,但对于count++这种非原子性、多指令的情况,由于线程切换,线程A刚把count=0加载到工作内存,线程B就可以开始工作了,这样就会导致线程A和B执行完的结果都是1,都写到主内存中,主内存的值还是1不是2,下面这张图形象表示了该历程:

原创声明:本文转载自公众号【胖滚猪学编程】​

有序性问题

JAVA为了优化性能,允许编译器和处理器对指令进行重排序,即有时候会改变程序中语句的先后顺序:

例如程序中:“a=6;b=7;”编译器优化后可能变成“b=7;a=6;”只是在这个程序中不影响程序的最终结果。

有序性指的是程序按照代码的先后顺序执行。但是不要望文生义,这里的顺序不是按照代码位置的依次顺序执行指令,指的是最终结果在我们看起来就像是有序的。

重排序的过程不会影响单线程程序的执行,却会影响到多线程并发执行的正确性。有时候编译器及解释器的优化可能导致意想不到的 Bug。比如非常经典的双重检查创建单例对象。

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

你可能会觉得这个程序天衣无缝,我两次判断是否为空,还用了synchronized,刚刚也说了,synchronized 是独占锁/排他锁。按照常理来说,应该是这么一个逻辑:
线程A和B同时进来,判断instance == null,线程A先获取了锁,B等待,然后线程 A 会创建一个 Singleton 实例,之后释放锁,锁释放后,线程 B 被唤醒,线程 B 再次尝试加锁,此时加锁会成功,然后线程 B 检查 instance == null 时会发现,已经创建过 Singleton 实例了,所以线程 B 不会再创建一个 Singleton 实例。

但多线程往往要有非常理性的思维,我们先分析一下 instance = new Singleton()这句话,根据刚刚原子性说到的,一句高级语言在cpu层面其实是多条指令,这也不例外,我们也很熟悉new了,它会分为以下几条指令:
1、分配一块内存 M;
2、在内存 M 上初始化 Singleton 对象;
3、然后 M 的地址赋值给 instance 变量。

如果真按照上述三条指令执行是没问题的,但经过编译优化后的执行路径却是这样的:
1、分配一块内存 M;
2、将 M 的地址赋值给 instance 变量;
3、最后在内存 M 上初始化 Singleton 对象

假如当执行完指令 2 时恰好发生了线程切换,切换到了线程 B 上;而此时线程 B 也执行 getInstance() 方法,那么线程 B 在执行第一个判断时会发现 instance != null ,所以直接返回 instance,而此时的 instance 是没有初始化过的,如果我们这个时候访问 instance 的成员变量就可能触发空指针异常,如图所示:

总结

并发程序是一把双刃剑,一方面大幅度提升了程序性能,另一方面带来了很多隐藏的无形的难以发现的bug。我们首先要知道并发程序的问题在哪里,只有确定了“靶子”,才有可能把问题解决,毕竟所有的解决方案都是针对问题的。并发程序经常出现的诡异问题看上去非常无厘头,但是只要我们能够深刻理解可见性、原子性、有序性在并发场景下的原理,很多并发 Bug 都是可以理解、可以诊断的。
总结一句话:可见性是缓存导致的,而线程切换会带来的原子性问题,编译优化会带来有序性问题。至于怎么解决呢!欲知后事如何,且听下回分解。

原创声明:本文转载自公众号【胖滚猪学编程】​

本文转载自公众号【胖滚猪学编程】 用漫画让编程so easy and interesting!欢迎关注!

【漫画】JAVA并发编程三大Bug源头(可见性、原子性、有序性)相关推荐

  1. java计算时间差_JAVA并发编程三大Bug源头(可见性、原子性、有序性),彻底弄懂...

    原创声明:本文转载自公众号[胖滚猪学编程]​ 某日,胖滚猪写的代码导致了一个生产bug,奋战到凌晨三点依旧没有解决问题.胖滚熊一看,只用了一个volatile就解决了.并告知胖滚猪,这是并发编程导致的 ...

  2. java堆栈有序无序,浅谈Java并发编程系列(四)—— 原子性、可见性与有序性

    Java内存模型是围绕着在并发过程中如何处理原子性.可见性和有序性这3个特征来建立的,我们来看下哪些操作实现了这3个特性. 原子性(atomicity): 由Java内存模型来直接保证原子性变量操作包 ...

  3. 漫画编程java_【漫画】JAVA并发编程之并发模拟工具

    上一节[漫画]JAVA并发编程三大Bug源头(可见性.原子性.有序性)我们聊了聊并发编程的三个bug源头,这还没开始进入并发世界,胖滚猪就遇到了难题.. 这个难题是所有初学者都会有的疑惑:没法复现那些 ...

  4. Java并发编程笔记(1)基础知识

    Java并发编程的三个性质 原子性 *在Java中,对基本数据类型的变量的读取和赋值操作是原子性操作,即这些操作是不可被中断的,要么执行,要么不执行. *只有形如x = 10这种不可再分的赋值操作才有 ...

  5. 【Java】Java并发编程

    文章主要目的帮助开发人员创建安全和高性能的并发类,提供各种实用设计规则,同时更加轻松应对并发编程相关面试. 简介 线程是实现并发的基础,能使复杂的异步代码变得更简单,极大简化了复杂系统的开发.充分发挥 ...

  6. 【Java并发编程】并发编程大合集

    转载请注明出处:http://blog.csdn.net/ns_code/article/details/17539599 为了方便各位网友学习以及方便自己复习之用,将Java并发编程系列内容系列内容 ...

  7. Java并发编程的学习

    转载出处:http://blog.csdn.net/ns_code/article/details/17539599 为了方便各位网友学习以及方便自己复习之用,将Java并发编程系列内容系列内容按照由 ...

  8. 【Java 并发编程】 03 万恶的 Bug 起源—并发编程的三大特性

    今天让我们一起走进并发编程中万恶的Bug起源-并发编程中三大特性.今天学习目标如下: 并发编程的三大特性都要哪些 ? 并发编程三大特性的由来? 如何解决并发编程三大特性问题? 基本概念 原子性:一组操 ...

  9. Java并发编程实战 代码bug,Java并发编程实战(1)- 并发程序的bug源头

    概述 并发编程一般属于编程进阶部分的知识,它会涉及到很多底层知识,包括操作系统. 编写正确的并发程序是一件很困难的事情,由并发导致的bug,有时很难排查或者重现,这需要我们理解并发的本质,深入分析Bu ...

最新文章

  1. 编写文档_如何通过编写优质文档来使自己的未来快乐
  2. c# 另存为excel
  3. android menu 小红点,Android自定义ActionProvider ToolBar实现Menu小红点
  4. openlayers加载svg,如何在OpenLayers-3中将SVG图像用作地图标记?
  5. 【Python CheckiO 题解】Feed Pigeons
  6. 多线程编程下单例模式与多例模式的使用总结
  7. C# this关键字(给底层类库扩展成员方法)
  8. 通过反编译深入理解Java String及intern
  9. 如何在html中在线编辑word文档,web版word在线编辑
  10. WSO2 ESB 5.0.0 配置消息存储
  11. Bootstrap4文件上传控件美化
  12. python实验——第一次
  13. xxx required a bean of type ‘com.xxx.utils.http.sss‘ that could not be found.
  14. Win7下eclipse ADT调试cocos2dx-lua工程
  15. 用数字万用表测量三极管的方法
  16. 美国“黑色星期五”单日销量不及双十一
  17. C++主函数简要介绍
  18. IOST 项目更新:BB ,展望正面 | TokenInsight
  19. Dolphin scheduler在Windows环境下的部署与开发
  20. Linux 定时备份日志

热门文章

  1. 钉钉发群通知报{“errcode“:310000,“errmsg“:“keywords not in content“}解决办法
  2. php去除html标签函数,php strip_tags() 函数去除 HTML、XML 以及 PHP 的标签。
  3. Fusion360学习记录:手机壳
  4. JPG格式图片怎么弄?分享两种转换图片格式方法
  5. matlab低通滤波
  6. SAIO - Swift All In One Diablo版 安装指南 Alpha
  7. 利用交叉观察者这个小宝贝儿,轻松实现懒加载、吸顶、触底
  8. 2020全球顶尖计算机科学家排名发布:香港高校20教授入围,香港科大占6席!
  9. GAN及其变体C_GAN,infoGAN,AC_GAN,DC_GAN(一)
  10. mysql percona_推荐使用percona版mysql | 学步园