概述

多线程三大特性:原子性、可见性、有序性。

1. 原子性

原子性是指:多个操作作为一个整体,不能被分割与中断,也不能被其他线程干扰。如果被中断与干扰,则会出现数据异常、逻辑异常。

多个操作合并的整体,我们称之为复合操作。一个复合操作,往往存在前后依赖关系,后一个操作依赖上一个操作的结果。如果上一个操作结果被其他线程干扰,对于当前线程看来整个复合操作的结果便不符合预期。同理线程也不能在复合操作中间被中断,中断必须发生在进入复合操作之前或者等到复合操作结束之后。

保证原子性就是在多线程环境下,保证单个线程执行复合操作符合预期逻辑。

典型的复合操作:『先检查后执行』和『读取—修改—写入』

1.1 先检查后执行

@NotThreadSafe
public class LazyInitClass {private static LazyInitClass instance ;public static LazyInitClass getInstance() {if(instance == null)instance = new LazyInitClass() ;return instance ;}
}

LazyInitClassgetInstance 中包含先检查后执行的复合操作,通常我们也可以称 getInstance 中包含竞态条件。假设线程 A 和线程 B 同时执行 getInstance。A 看到 instance 为空,便执行 new LazyInitClass() 逻辑。A 还未完成初始化并设置 instance,B 检查 instance,此时 instance 为空,B 便也会执行 new LazyInitClass()。那么两次调用 getInstance 时可能会得到不同的结果。通常 getInstance 的预期结果是多次调用得到相同的对象实例。

LazyInitClassgetInstance 方法虽然存在竞态条件,多数情况下并不会造成业务异常,影响仅仅是增加了 JVM 垃圾回收负担而已。这也是多线程问题隐蔽性强且偶发的原因之一。

但话说回来,编程原则之一就是所有逻辑都必须建立在确定性之上,任何建立在不确定性上的逻辑都是隐患。虽然从业务上看多数情况下没问题,但竞态条件的存在,让代码逻辑建立在不确定性之上。作为编码者应该重视此类问题。

1.2 读取—修改—写入

@NotThreadSafe
public class ReadModifyAndWriteClass {private int count = 0 ;public int increase() {return count++ ;}
}

由于 i++ 本身不是原子操作,属于复合操作。ReadModifyAndWriteClassincrease 包含了读取—修改—写入。假设线程 A 和线程 B 同时执行 increase。A 看到 count 为 0,执行 ++ 逻辑。当 ++ 操作还未完成,此时 B 读取 count 看到的仍然是 0。A、B 各自完成 ++ 逻辑后,count 的值等于 1。这就造成了虽然调用了两次 increase 方法,但 count 只增加了 1。这也与预期:每调用一次 increase,count 增加 1 的结果不符。

2. 可见性

可见性问题是指,一个线程修改的共享变量,其他线程是否能够立刻看到。对于串行程序而言,并不存在可见性问题,前一个操作修改的变量,后一个操作一定能读取到最新值。但在多线程环境下如果没有正确的同步则不一定。

有很多因素会使得线程无法立即看到甚至永远无法看到另一个线程的操作结果。在编译器中生成的指令顺序,可以与源代码中的顺序不同,此外编译器还会把变量保存在寄存器而非内存中;处理器可以采用乱序或并行等方式来执行指令;缓存可能会改变将写入变量提交到主内存的次序;而且,保存在处理器本地缓存中的值,对于其他处理器是不可见的。这些因素都会使得一个线程无法看到变量的最新值,并且会导致其他线程中的内存操作似乎在乱序执行。

2.1 缓存引起的可见性

上图是多核 CPU 内存图,其中 individual memory 表示核心多级缓存。main memory 表示主内存,即共享内存。共享内存(shared memory)是线程之间共享的内存,也称为堆内存(heap memory)。所有实例域(instance fields)、静态域(static fields)和数组元素(array elements)都保存在堆内存中。

A 线程与 B 线程共同操作共享变量 V(初始值为 0),A、B 线程分别将 V 变量从主内存复制到 CPU 内核的多级缓存中,此时 A 与 B 都读到 V 的值为 0。A 更新自己的 individual memory 中的 V 的值为 1,此时如果没有将 V 值同步至主内存中,B 从自己的 individual memory 中读到 V 的值仍然为 0。当 V 值同步到主内存后,多级缓存失效,此时 B 才能够从主内存中读取到最新的 V 值为 1。由于多线程环境下何时将多级缓存同步到主内存时间上不确定,所以造成了可见性问题,即 A 线程对共享变量 V 的写操作,位于写操作后执行的 B 线程的读操作不能立即感知。

3. 有序性

有序性问题是指从观察到的结果推测,代码执行的顺序与代码组织的顺序不一致。

3.1 指令重排序引起的有序性问题

在计算机体系结构中,为了提高执行部件的处理速度,经常在部件中采用流水线技术。所谓流水线技术,是指将一个重复的时序过程,分解成若干个子过程,而每一个子过程都可有效地在其专用功能段上与其他子过程同时执行。

以 DLX 指令集结构为例,一条指令的执行简单说可以分为以下几个步骤:

  1. 取指令(IF)
  2. 指令译码/读寄存器(ID)
  3. 执行/有效地址计算(EX)
  4. 存储器访问/分支完成(MEM)
  5. 写回(WB)

每一个步骤都可能使用不同的硬件完成。

由上图所示,如果没有指令流水线,指令2 需要等待指令1 完全执行完成后执行。假设每一个步骤(子过程)需要花费 1 个 CPU 时钟周期,则指令2 需要等待 5 个时钟周期。而使用指令流水线后,指令2 只需等待 1 个时钟周期就可以开始执行。指令2 开始执行时,指令1 根本还没开始执行,仅仅完成了取指操作而已。这仅仅是 DLX 指令集结构的流水线,实际商用 CPU 的流水线级别甚至可以达到 10 级以上,性能提升可谓是非常明显。

由于流水线技术的引入,不得不面对流水线的三种类型的相关:结构相关、数据相关、控制相关。

  1. 结构相关:当指令在重叠执行过程中,硬件资源满足不了指令重叠执行的要求,发生资源冲突时将产生“结构相关”。
  2. 数据相关:当一条指令需要用到前面指令的执行结果,而这些指令均在流水线中重叠执行时,就可能引起“数据相关”。
  3. 控制相关:当流水线遇到分支指令和其他会改变 PC 值的指令时就会发生“控制相关”。

一旦流水线中出现相关,指令在流失线中的执行就会出现问题,消除相关的最基本方法是让流水线中的某些指令暂停执行。一旦暂停,所有硬件设备都会进入一个停顿周期,直接影响是性能的下降。

我们说的指令重排序就是在产生数据相关时替代流水线暂停的重要方法。指令重排序仅仅是减少流水线暂停技术的一种,在 CPU 设计中还有很多其他软硬件技术来防止流水线暂停。

下图展示了 A = B + C 操作的执行过程。LW 表示加载,LW R1, B 表示把 B 的值加载到寄存器 R1 中。ADD 表示加法,ADD R3, R1, R2 表示把寄存器 R1 和 R2 中的值相加保存到寄存器 R3 中。SW 表示存储,SW A, R3 表示将寄存器 R3 中的值保存到变量 A 中。

可以看到,ADD 指令的流水线上出现了一个 stall,表示一个暂停。之所以出现暂停,是因为 R2 的数据还没准备好( LW R2, C 的操作还没完成 )。由于 ADD 暂停的出现,后续的操作都暂停了一个周期。

下面是一个更为复杂的例子:

可以看到,由于 ADD 和 SUB 指令都需要等待上一条指令的执行结果,所以整个流水线上插入了不少 stall。下图显示了如何消除类似的暂停。

可以看到,由于 ADD 和 SUB 指令都需要等待上一条指令的执行结果,所以整个流水线上插入了不少 stall。下图显示了如何消除类似的暂停。

虽然指令重排序会导致有序性问题,但指令重排序对性能的提高有非常重大的意义。

3.2 CPU 缓存引起的有序性问题

2.1 节已经讨论过 CPU 缓存导致的可见性问题。CPU 缓存也会导致有序性问题。

看如下的例子:

假设 b、c 为局部变量,初始值为 1,A、D 为共享变量,初始值为 0 和 false。Thread1 先于 Thread2 运行,运行结果:Thread2 输出 0。

从结果推测 Thread1 中的 D = true 先于 A = b + c 执行了。

当 D = true 执行完成后,A = b + c 还没来得及执行,此时 Thread2 输出 A 的值,才会出现结果为 0 的情况。

分析:Thread1 将 A、D 共享变量从主内存复制到当前 CPU 内核的多级缓存中,按顺序执行完 A = b + c 和 D = true 后,多级缓存中 A = 2, D = true。然后 Thread1 将 D 的值优先同步到主缓存,A 的值没有同步到主缓存。此时 Thread2 执行,能看到 D 的最新值 true,却不能看到 A 的最新值,只能看到主缓存中 A 的初始值 0。

所以从 Thread2 看,Thread1 线程的执行出现了有序性问题,但从 Thread1 看,自己的确是按照代码组织顺序执行的。

4. 总结

本章详细讲解了多线程的三大特性:原子性、可见性、有序性。想要正确编写多线程程序,一定要正确理解这三大特性。

5. 参考资料

  1. 《The Java® LanguageSpecification Java SE 8 Edition》作者:James Gosling、Bill Joy、Guy Steele、Gilad Bracha、Alex Buckley
  2. 《Java Concurrency in Practice》作者:Brain Goetz、Tim Peierls、Joshua Bloch、Joseph Bowbeer、David Holmes、Doug Lea
  3. 《计算机体系结构》作者:张晨曦、王志英、张春元、戴葵、朱海滨

Java 并发系列(一):多线程三大特性相关推荐

  1. 5W字高质量java并发系列详解教程(上)-附PDF下载

    文章目录 第一章 java.util.concurrent简介 主要的组件 Executor ExecutorService ScheduledExecutorService Future Count ...

  2. 明翰Java教学系列之多线程篇V0.2(持续更新)

    文章目录 传送门 前言 背景知识 并行与并发 线程与进程 内存模型 1. 计算机内存模型 `2. Java内存模型` 2.1 内存交互 2.1.1 交互操作 2.1.2 交互规则 `2.2 并发编程特 ...

  3. Java并发系列(11)——ThreadPoolExecutor实现原理与手写

    接上一篇<Java并发系列(10)--FutureTask 和 CompletionService> 文章目录 9 线程池 9.1 JDK 线程池 9.2 ThreadPoolExecut ...

  4. 17、Java并发性和多线程-避免死锁

    以下内容转自http://ifeve.com/deadlock-prevention/: 在有些情况下死锁是可以避免的.本文将展示三种用于避免死锁的技术: 加锁顺序 当多个线程需要相同的一些锁,但是按 ...

  5. JMM内存模型 多线程三大特性

    本文目录 1.JMM 内存模型 1.主内存 2.工作内存 3.JMM 线程操作内存的两条基本规定 4.JMM 模型八大内存交互指令 5.JMM 模型内存交互操作 6.JMM 模型内存同步规则 2.多线 ...

  6. Java并发系列(10)——FutureTask 和 CompletionService

    接上一篇<Java并发系列(9)--并发工具类> 文章目录 8 FutureTask 与 CompletionService 8.1 FutureTask 8.1.1 类图 8.1.2 几 ...

  7. 《Java 高并发》02 多线程的特性

    多线程的三大特性:原子性.可见性和有序性. 原子性 原子性是指一个操作或者多个操作,一旦开始就不会被其他线程干扰,即使是在多个线程一起执行的情况下也不会被干扰.或者不执行. 原子性主要是为了保证数据一 ...

  8. 并发基础篇(一): Java 并发性和多线程

    说在前面 介绍文章之前,先给出一个多线程的思维导图, 后续的文章就根据思维导图来一步一步的分析java多线程的知识. 一.介绍 在过去单 CPU 时代,单任务在一个时间点只能执行单一程序.之后发展到多 ...

  9. Java并发性和多线程介绍

    作者:Jakob Jenkov 译者:Simon-SZ  校对:方腾飞 http://tutorials.jenkov.com/java-concurrency/index.html 在过去单CPU时 ...

最新文章

  1. 基于cocos2d-x的快速的游戏开发--回合制游戏
  2. 统计学习方法笔记(四)-最大熵模型原理及python实现
  3. js比较查看两个数组之间是否相等
  4. 【渝粤题库】国家开放大学2021春4988电子政务概论题目
  5. 【今日CS 视觉论文速览】20 Dec 2018
  6. java class和this_转:java 类名 this 的使用
  7. Docker应用基础
  8. digital ocean 内存不足时增加swap文件的方法
  9. 关于GCD执行任务的理解
  10. java day59【 AOP 的相关概念[理解] 、 Spring 中的 AOP[掌握] 、 Spring 整合 Junit[掌握] 】...
  11. Oracle数据库安装与配置
  12. html怎么设置光线,vray渲染器太阳光参数怎么设置?
  13. 微信小程序 支付宝小程序 实现省市区县四级联动
  14. 精美的wordpress企业主题模板
  15. golang下文件锁的使用
  16. 霹雳吧啦Wz语义分割学习笔记P2
  17. HDU 4508 湫湫系列故事――减肥记I 【完全背包】
  18. 单源最短路径 Dijkstra+优先队列
  19. python offset函数_Python Pandas tseries.offsets.BusinessHour.copy用法及代码示例
  20. 远程办公导致企业网络被黑?请注意这三点

热门文章

  1. 愿我们心中都有信念,眼里都有光芒
  2. 日期累加器(任意时间基础上加上天数,不包括节假日以及周六日)
  3. 服务器运行多个安卓系统,一台服务器可以做几个云手机
  4. ElementUI上传组件扩展--重新上传
  5. YOLOV5-face视频流
  6. 【汇正财经】蓝筹股的类别
  7. Button上面添加一个图片
  8. 茄子快传 java,GitHub - liefyuan/KuaiChuan: 仿茄子快传的一款文件传输应用
  9. mysql-connector-java 到底有什么用
  10. 解决win10下安装ubuntu14.04后无法搜寻和连接wifi无线网络的问题