多线程编程模型

线程安全名词

串行、并发和并行

  • 串行:一个人,将任务一个一个完成
  • 并发:一个人,有策略地同时做多件事情
  • 并行:多个人,每人做一个事情

竞态

名词

  • 竞态:计算结果的正确性与时间有关的现象被称为竞态
  • 共享变量:可以被多个线程共同访问的变量

竞态产生的条件

  • read-modify-write
  • check-then-act

线程安全性

如果一个类在多线程环境下无需做任何改变也能运作正常,则称其为线程安全的

线程安全问题

原子性

要点

  • 访问(读、写)某个共享变量的操作从其执行线程以外的任何线程来看,该操作要么已经执行结束要么尚未发生,即其他线程不会“看到”该操作执行了部分的中间效果。
  • 原子性只有在多线程环境下才有意义。

如何实现原子性?

  • 使用锁
  • 利用CAS指令

Java语言中的原子性操作

  • 对所有变量的读操作都具有原子性
  • 对 long 和 double 以外的任何类型的变量(基础类型、引用类型)的写操作都是原子性的

可见性

要点

  • 可见性 就是指一个线程对共享变量的更新的结果对于读取相应共享变量的线程而言是否可见的问题。
  • 多线程程序在可见性方面的问题意味着某些线程会读取到旧的数据,从而导致不可预期的后果。

问题产生的原因

对内存的访问不是直接进行的,为了提高访问的速度,会先在高速缓存中进行相关操作;另外,每个处理器都有其寄存器,这也可能导致不同的线程看到的数据不一致。

处理器不是直接与主内存打交道,而是通过寄存器(Register)、高速缓存(Cache)、写缓冲器(Store Buffer)和无效化队列[1](Invalidate Queue)等部件执行内存的读写操作的。

有序性

重排序概念

重排序是什么

  • 编译器可能改变两个操作的先后顺序,而不是完全按照程序的目标代码所指定的顺序执行
  • 一个处理器上执行的多个操作,从其他处理器的角度来看其顺序可能与目标代码所指定的顺序不一致

重排序的机遇与挑战

重排序是对内存访问有关的操作所做的一种优化,可以在不影响单线程程序正确性的情况下提升程序的性能。但是,它可能对多线程程序的正确性产生影响。

重排序的来源

  • 编译器(如JIT编译器)
  • 处理器和存储子系统(包括写缓冲器 Store Buffer、高速缓存Cache)

几个相关的术语

  • 源代码顺序:源代码中指定的内存访问操作的顺序
  • 程序顺序:在给定处理器上运行的目标代码所指定的内存访问顺序,如JVM字节码
  • 执行顺序:内存访问操作在给定处理器上的实际执行顺序
  • 感知顺序:给定处理器所感知到的该处理器及其他处理器的内存访问操作发生的顺序

在此基础上,重排序可以做如下划分:

指令重排序

回顾:Java平台包含两种编译器:静态编译器(javac)和动态编译器(JIT编译器)。前者的作用是将Java源代码(.java文本文件)编译为字节码(.class二进制文件),它是在代码编译阶段介入的。后者的作用是将字节码动态编译为Java虚拟机宿主机的本地代码(机器码),它是在Java程序运行过程中介入的。

在Java平台中,静态编译器基本不会执行指令重排序,而JIT编译器则可能执行指令重排序

对于编译器如何优化代码的解释: (摘自《Java多线程编程实战》)

处理器对指令进行重排序也被称为处理器的乱序执行 (Out-of-order Execution)。

现代处理器为了提高指令执行效率,往往不是按照程序顺序逐一执行指令的,而是动态调整指令的顺序,做到哪条指令就绪就先执行哪条指令,这就是处理器的乱序执行。在乱序执行的处理器中,指令是一条一条按照程序顺序被处理器读取的(亦即“顺序读取”),然后这些指令中哪条就绪了哪条就会先被执行,而不是完全按照程序顺序执行(亦即“乱序执行”)。

这些指令执行的结果(要进行写寄存器或者写内存的操作)会被先存入重排序缓冲器(ROB, Reorder Buffer),而不是直接被写入寄存器或者主内存。重排序缓冲器会将各个指令的执行结果按照相应指令被处理器读取的顺序提交(Commit,即写入)到寄存器或者内存中去(亦即“顺序提交”)。

在乱序执行的情况下,尽管指令的执行顺序可能没有完全依照程序顺序,但是由于指令的执行结果的提交(即反映到寄存器和内存中)仍然是按照程序顺序来的,因此处理器的指令重排序并不会对单线程程序的正确性产生影响。

猜测执行

比如,处理器可以先执行 IF 语句中的内容,并将接过来保存在 ROB 中,然后再判断 IF 是否成立,如果成立就可以直接使用,不成立则丢弃。

当然,在多线程环境下,这也可能造成线程安全问题。

存储子系统重排序

存储子系统

  • 写缓冲器:对主内存的操作都是通过写缓冲器进行的
  • 高速缓存:处理器通过高速缓存访问主内存

内存重排序

即使在处理器严格依照程序顺序执行两个内存访问操作的情况下,在存储子系统的 作用下其他处理器对这两个操作的感知顺序仍然可能与程序顺序不一致,即这两个操作的执 行顺序看起来像是发生了变化。这种现象就是存储子系统重排序, 也被称为内存重排序(Memory Ordering)。

内存重排序的类型

如果把读内存称为 Load,写内存称为 Store,则内存重排序有如下四种可能:

  • LoadLoad重排序
  • StoreStore重排序
  • LoadStore重排序
  • StoreLoad重排序

内存重排序与具体的处理器微架构有关,不同微架构的处理器允许的内存重排序也是不同的

貌似串行语义

这个概念类似于 MySQL 中的可串行化和分布式中的 XX 概念

重排序也是遵循一定的规则的,我们要做到一种假象:貌似串行语义。也就是从单线程程序的角度保证重排序后的结果不影响程序的正确性。(但是不保证多线程环境下的正确性)

规则如下:

  • 存在数据依赖关系的语句不会被重排序,只有不存在数据依赖关系的语句才会被重排序。
  • 存在控制依赖关系的语句可以允许被重排序,如之前的猜测执行

保证有序性

在多线程角度下,从逻辑上(看上去)禁止重排序,从而保证有序性。

Java 的 volatile 关键字、sychronized 等都能够实现有序性。

多线程模型的其他问题

上下文切换

线程的活性故障

这些由资源稀缺性或者程序自身的问题和缺陷导致线程一直处于非 RUNNABLE 状态,或者线程虽然处于 RUNNABLE 状态但是其要执行的任务却一直无法进展的现象就被称为 线程活性故障

  • 死锁(哲学家进餐问题)
  • 锁死(没有唤醒线程,比如唤醒线程也睡眠了)
  • 活锁(一个线程对值做add,另一个做sub,导致程序一直进行,无法停止)
  • 饥饿(某些线程无法获得其所需资源,而使得任务无法进展)

资源争用与调度

概念

  • 一次只能被一个线程占用的资源被称为 排他性资源
  • 资源被一个线程访问时,其他线程试图访问该资源的现象被称为 资源争用。我们要达到的理想状态是:高并发、低争用

资源调度的公平性

资源调度的一个常见特性是:他是否保证公平性(是否先到先得)。

非公平调度策略是我们多数情况下的首选资源调度策略,其优点是吞吐量大,缺点是资源申请者申请资源所需时间的是偏差可能较大,并可能导致饥饿现象。

公平调度适合在资源的持有线程占用资源的时间相对长资源的平均申请时间间隔相对长的情况下,或对申请的时间偏差有要求的情况下使用,优点和缺点则反之。


  1. 参见此文,后续会进行补充

Java多线程编程实战02:多线程编程模型的更多相关文章

  1. Java并发编程实战 01并发编程的Bug源头

    摘要 编写正确的并发程序对我来说是一件极其困难的事情,由于知识不足,只知道synchronized这个修饰符进行同步. 本文为学习极客时间:Java并发编程实战 01的总结,文章取图也是来自于该文章 ...

  2. Java高级项目实战02:客户关系管理系统CRM系统模块分析与介绍

    本文承接上一篇:Java高级项目实战之CRM系统01:CRM系统概念和分类.企业项目开发流程 先来CRM系统结构图: 每个模块作用介绍如下: 1.营销管理 营销机会管理:针对企业中客户的质询需求所建立 ...

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

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

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

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

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

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

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

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

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

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

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

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

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

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

随机推荐

  1. Spark学习摘记 —— RDD行动操作API归纳

    本文参考 参考<Spark快速大数据分析>动物书中的第三章"RDD编程",前一篇文章已经概述了转化操作相关的API,本文再介绍行动操作API 和转化操作API不同的是, ...

  2. 一道关于压缩包的ctf题目(包括暴力破解,明文攻击,伪加密)

    关于题目附件 链接:https://pan.baidu.com/s/1PRshlizSndkgxkslnqJrHA 提取码:p76e zip三连击 下载附件得到题目 手机号码一般是11位,那么我们设置 ...

  3. 微信小程序版博客——开发汇总总结(附源码)

    花了点时间陆陆续续,拼拼凑凑将我的小程序版博客搭建完了,这里做个简单的分享和总结. 整体效果 对于博客来说功能页面不是很多,且有些限制于后端服务(基于ghost博客提供的服务),相关样式可以参考截图或 ...

  4. 用Exception类捕获所有异常的技术是怎么用的?

    3.用Exception类捕获所有异常  马克-to-win:注意,一个事实是:Exception类是所有其他异常类的父类,所以Exception类能捕获所有的异常.马克-to-win:问题是用Exc ...

  5. HDMS(Headend Device Management System)软件下载

    进入官网http://www.pbi-china.com/CHS/index.aspx点击右下角的下载通道.

  6. Map的key是否可重复

    我们都知道Map的一大特性是key唯一不可重复,可是真的是这样的吗? 我们来试验一下: 运行结果: 我们可以看到在map里有两个同样的person作为key,打破了map的key不可重复的特性. 我们 ...

  7. @RequestBody和@RequestParam注解以及返回值,ajax相关知识点

    关于前后端传递json数据这块查了好多资料,好多地方还是不清楚,先记录一下清楚的地方. 如果我们前端使用ajax发json数据,一般都加上contentType:'application/json;c ...

  8. 为vscode开发一款svn右键菜单扩展

    在我平时的工作中会经常用到svn blame这个命令,但是vscode现有的svn扩展普遍都不能自定义右键菜单. 所以我产生一个想法:自己动手为vscode开发一款svn的扩展来定制右键菜单,本文记录 ...

  9. 告别收费BI!如何自己动手做一个免费的可视化数据报表还支持文档在线预览?

    本人大学刚毕业目前在一家互联网公司从事产品运营工作,一季度刚过,公司需要我出一份产品运营数据报表,由于产品用户数据.订单数据等数据量太大,我希望找一款Bi产品,支持我做出一个精美的可视化报表,还可以让 ...

  10. gin框架使用【3.路由参数】

    GET url: http://127.0.0.1:8080/users/{id} http://127.0.0.1:8080/users/1   对于id值的获取 package main impo ...