多线程编程模型

线程安全名词

串行、并发和并行

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

竞态

名词

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

竞态产生的条件

  • 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. 5-Pandas数据分组的函数应用(df.apply()、df.agg()和df.transform()、df.applymap())

      将自己定义的或其他库的函数应用于Pandas对象,有以下3种方法: apply():逐行或逐列应用该函数 agg()和transform():聚合和转换 applymap():逐元素应用函数 一 ...

  2. C语言思维导图—自己整理的

  3. C++中初始化列表的使用(总结)

    原文链接 https://www.cnblogs.com/dishengAndziyu/p/10906081.html 参考链接:https://www.cnblogs.com/laiqun/p/57 ...

  4. 横竖屏切换android:screenOrientation属性的使用

    在开发android的应用中,有时候需要限制横竖屏切换,只需要在AndroidManifest.xml文件中加入android:screenOrientation属性限制.    android:sc ...

  5. 简单的axios请求返回数据解构赋值

    本地  data.json 文件 { "name": "大熊", "age": 18, "fnc": [ 1, 2, 3 ...

  6. vue简单的父子组件之间传值

     todo-list为例子: 代码: 父传子--------------属性  v-bind 子传父--------------$emit <!DOCTYPE html> <html ...

  7. 服务器jupyter连接不上主机

    首先安装jupyter pip3 install jupyter -i http://pypi.douban.com/simple/ --trusted-host pypi.douban.com 然后 ...

  8. RMI反序列化学习

    RMI学习 1.RMI简介 RMI(Remote Method Invocation),远程方法调用方法,其实就是本地java虚拟机要调用其他java虚拟机的方法,两个虚拟机可以是运行在相同计算机上的 ...

  9. 索引优化、Sql查询语句优化

    工作中我们经常会遇到系统查询慢的情况,一般我们会采取好多方法进行优化,如建立索引,优化查询Sql,分表,规范数据表结构设计,调整数据库参数(内存分配.缓存等),增加硬件配置,优化网络环境等.下面介绍两 ...

  10. 面试突击39:synchronized底层是如何实现的?

    想了解 synchronized 是如何运行的?就要先搞清楚 synchronized 是如何实现? synchronized 同步锁是通过 JVM 内置的 Monitor 监视器实现的,而监视器又是 ...