写在前面

  • 生活中你一定听说过——能者多劳
  • 作为 Java 程序员,你一定听过——这个功能请求慢,能加一层缓存或优化一下 SQL 吗?
  • 看过中国古代神话故事的也一定听过——天上一天,地上一年

一切设计来源于生活,上一章 学并发编程,透彻理解这三个核心是关键 中有讲过,作为"资本家",你要尽可能的榨取 CPU,内存与 IO 的剩余价值,但三者完成任务的速度相差很大,CPU > 内存 > IO分,CPU 是天,那内存就是地,内存是天,那 IO 就是地,那怎样平衡三者,提升整体速度呢?

  1. CPU 增加缓存,还不止一层缓存,平衡内存的慢
  2. CPU 能者多劳,通过分时复用,平衡 IO 的速度差异
  3. 优化编译指令

上面的方式貌似解决了木桶短板问题,但同时这种解决方案也伴随着产生新的可见性,原子性,和有序性的问题,且看

三大问题

可见性

一个线程对共享变量的修改,另外一个线程能够立刻看到,我们称为可见性

谈到可见性,要先引出 JMM (Java Memory Model) 概念, 即 Java 内存模型,Java 内存模型规定,将所有的变量都存放在 主内存 中,当线程使用变量时,会把主内存里面的变量 复制 到自己的工作空间或者叫作 私有内存 ,线程读写变量时操作的是自己工作内存中的变量。

用 Git 的工作流程理解上面的描述就很简单了,Git 远程仓库就是主内存,Git 本地仓库就是自己的工作内存

文字描述有些抽象,我们来图解说明:

看这个场景:

  1. 主内存中有变量 x,初始值为 0
  2. 线程 A 要将 x 加 1,先将 x=0 拷贝到自己的私有内存中,然后更新 x 的值
  3. 线程 A 将更新后的 x 值回刷到主内存的时间是不固定的
  4. 刚好在线程 A 没有回刷 x 到主内存时,线程 B 同样从主内存中读取 x,此时为 0,和线程 A 一样的操作,最后期盼的 x=2 就会编程 x=1

这就是线程可见性的问题

JMM 是一个抽象的概念,在实际实现中,线程的工作内存是这样的:

为了平衡内存/IO 短板,会在 CPU 上增加缓存,每个核都只有自己的一级缓存,甚至有一个所有 CPU 都共享的二级缓存,就是上图的样子了,都说这么设计是硬件同学留给软件同学的一个坑,但能否跳过去这个坑也是衡量软件同学是否走向 Java 进阶的关键指标吧......

小提示

从上图中你也可以看出,在 Java 中,所有的实例域,静态域和数组元素都存储在堆内存中,堆内存在线程之间共享,这些在后续文章中都称之为「共享变量」,局部变量,方法定义参数和异常处理器参数不会在线程之间共享,所以他们不会有内存可见性的问题,也就不受内存模型的影响

一句话,要想解决多线程可见性问题,所有线程都必须要刷取主内存中的变量

怎么解决可见性问题呢?Java 关键字 volatile 帮你搞定,后续章节会分析......

原子性

原子(atom)指化学反应不可再分的基本微粒,原子性操作你应该能感受到其含义:

所谓原子操作是指不会被线程调度机制打断的操作;这种操作一旦开始,就一直运行到结束,中间不会有任何 context switch

小品「钟点工」有一句非常经典的台词,要把大象装冰箱,总共分几步?

来看一小段程序:

多线程情况下能得到我们期盼的 count = 20000 的值吗? 也许有同学会认为,线程调用的 counter 方法只有一个 count++ 操作,是单一操作,所以是原子性的,非也。在线程第一讲中说过我们不能用高级语言思维来理解 CPU 的处理方式,count++ 转换成 CPU 指令则需要三步,通过下面命令解析出汇编指令等信息:

javap -c UnsafeCounter

截取 counter 方法的汇编指令来看:

解释一下上面的指令,

16 : 获取当前 count 值,并且放入栈顶

19 : 将常量 1 放入栈顶

20 : 将当前栈顶中两个值相加,并把结果放入栈顶

21 : 把栈顶的结果再赋值给 count

由此可见,简单的 count++ 不是一步操作,被转换为汇编后就不具备原子性了,就好比大象装冰箱,其实要分三步:

第一步,把冰箱门打开;第二步,把大象放进去;第三步,把冰箱门带上

结合 JMM 结构图理解,说明一下为什么很难得到 count=20000 的结果:

多线程计数器,如何保证多个操作的原子性呢?最粗暴的方式是在方法上加 synchronized 关键字,比如这样:

问题是解决了,如果 synchronized 是万能良方,那么也许并发就没那么多事了,可以靠一个 synchronized 走天下了,事实并不是这样,synchronized 是独占锁 (同一时间只能有一个线程可以调用),没有获取锁的线程会被阻塞;另外也会带来很多线程切换的上下文开销

所以 JDK 中就有了非阻塞 CAS (Compare and Swap) 算法实现的原子操作类 AtomicLong 等工具类,看过源码的同学也许会发现一个共同特点,所有原子类中都有下面这样一段代码:

private static final Unsafe unsafe = Unsafe.getUnsafe();

这个类是 JDK 的 rt.jar 包中的 Unsafe 类提供了 硬件级别 的原子性操作,类中的方法都是 native 修饰的,后面介绍原子类之前也会先说明这个类中的几个方法,这里先简单介绍有个印象即可。

有同学不理解我刚刚提到的线程上下文切换开销很大是什么意思,举 2个例子你就懂了:

  • 你(CPU)在看两本书(两个线程),看第一本书很短时间后要去看第二本书,看第二本书很短时间后又回看第一本书,并要精确的记得看到第几行,当初看到了什么(CPU 记住线程级别的信息),当让你 "同时" 看 10 本甚至更多,切换的开销就很大了吧
  • 综艺节目中有很多游戏,让你一边数钱,又要一边做其他的事,最终保证多样事情都做正确,大脑开销大不大,你试试就知道了

    并发Bug之源有三,请睁大眼睛看清它们的更多相关文章

    1. 【转帖】自助式BI的崛起:三张图看清商业智能和大数据分析市场趋势

      自助式BI的崛起:三张图看清商业智能和大数据分析市场趋势 大数据时代,商业智能和数据分析软件市场正在经历一场巨变,那些强调易用性的,人人都能使用的分析软件正在取代传统复杂的商业智能和分析软件成为市场的 ...

    2. 并发bug之源(一)-可见性

      CPU三级缓存 要聊可见性,这事儿还得从计算机的组成开始说起,我们都知道,计算机由CPU.内存.磁盘.显卡.外设等几部分组成,对于我们程序员而言,写代码主要关注CPU和内存两部分.放几张马士兵老师的图 ...

    3. 并发工具CyclicBarrier源码分析及应用

        本文首发于微信公众号[猿灯塔],转载引用请说明出处 今天呢!灯塔君跟大家讲: 并发工具CyclicBarrier源码分析及应用 一.CyclicBarrier简介 1.简介 CyclicBarri ...

    4. Java并发——结合CountDownLatch源码、Semaphore源码及ReentrantLock源码来看AQS原理

      前言: 如果说J.U.C包下的核心是什么?那我想答案只有一个就是AQS.那么AQS是什么呢?接下来让我们一起揭开AQS的神秘面纱 AQS是什么? AQS是AbstractQueuedSynchroni ...

    5. Java并发编程之CAS第三篇-CAS的缺点及解决办法

      Java并发编程之CAS第三篇-CAS的缺点 通过前两篇的文章介绍,我们知道了CAS是什么以及查看源码了解CAS原理.那么在多线程并发环境中,的缺点是什么呢?这篇文章我们就来讨论讨论 本篇是<凯 ...

    6. AQS源码三视-JUC系列

      AQS源码三视-JUC系列 前两篇文章介绍了AQS的核心同步机制,使用CHL同步队列实现线程等待和唤醒,一个int值记录资源量.为上层各式各样的同步器实现画好了模版,像已经介绍到的ReentrantL ...

    7. CentOS7创建本地YUM源的三种方法

      这篇文章主要介绍了CentOS7创建本地YUM源的三种方法,本文讲解了使用CentOS光盘作为本地yum源.如何为CentOS创建公共镜像.创建完全自定义的本地源等内容,需要的朋友可以参考下     ...

    8. java 并发编程——Thread 源码重新学习

      Java 并发编程系列文章 Java 并发基础——线程安全性 Java 并发编程——Callable+Future+FutureTask java 并发编程——Thread 源码重新学习 java并发 ...

    9. jnhs 无法提交断点LineBreakpoint hibernate4CURD : -1, 原因是: 找不到 URL 'file:/E:/版本控制/Design-java/hibernate4CURD/' 的源根目录。请验证项目源的设置。

      无法提交断点LineBreakpoint hibernate4CURD : -1, 原因是: 找不到 URL 'file:/E:/版本控制/Design-java/hibernate4CURD/' 的 ...

    随机推荐

    1. .NET----错误和异常处理机制

      前言 错误的出现并不总是编写程序的人的原因,有时应用程序会因为应用程序的最终用户引发的动作或运行代码的环境发生错误.无论如何,我们都应预测应用程序中出现的错误,并相应的进行编码. .Net改进了处理错 ...

    2. 你所不知道的 CSS 负值技巧与细节

      写本文的起因是,一天在群里有同学说误打误撞下,使用负的 outline-offset 实现了加号.嗯?好奇的我马上也动手尝试了下,到底是如何使用负的 outline-offset 实现加号呢? 使用负 ...

    3. 【TCP/IP】ICMP协议

      ICMP协议有两种报文: 1,查询报文 2,差错报文

    4. 0x33 同余

      目录 定义 同余类与剩余系 费马小定理 欧拉定理 证明: 欧拉定理的推论 证明: 应用: 定义 若整数 $a$ 和整数 $b$ 除以正整数 $m$ 的余数相等,则称 $a,b$ 模 $m$ 同余,记为 ...

    5. win10 我的电脑下面的六个文件夹的隐藏

        第一步   第二步     第三步 修改注册表,要隐藏那个文件夹,ThisPCPolicy 改为 "Hide" 修改我的文档的注册表值,使我的文档文件夹隐藏     <w ...

    6. 原生JavaScript(js)手把手教你写轮播图插件(banner)

      ---恢复内容开始--- 1.轮播图插件 1.什么是插件: 为已有的程序增加功能 2.插件的特点(为什么要做成一个插件)与注意事项: 1.通用性,可移植性强 2.兼容性:不会对其他代码产生影响 3.创 ...

    7. 初识JavaScript和面向对象

      1.javascript基本数据类型: number: 数值类型 string: 字符串类型 boolean: 布尔类型 null: 空类型 undefault:未定义类型 object: 基本数据类 ...

    8. 【0806 | Day 9】异常处理/基本的文件操作

      一.异常处理 异常即报错,可分为语法异常和逻辑异常 1. 语法异常 举个栗子 if #报错 syntaxerror 0 = 1 #报错 syntaxerror ... 正经地举个栗子 print(1) ...

    9. String关键字

      关于String和new String()见我写的前一篇博客 String和new String()的区别 1.String的"+"运算 a.String str = " ...

    10. linux后台运行的几种方式

      1.nohup将程序以忽略挂起信号的方式运行起来 补充说明nohup命令 可以将程序以忽略挂起信号的方式运行起来,被运行的程序的输出信息将不会显示到终端.无论是否将 nohup 命令的输出重定向到终端 ...