Java多线程编程实战02:多线程编程模型
多线程编程模型
线程安全名词
串行、并发和并行
- 串行:一个人,将任务一个一个完成
- 并发:一个人,有策略地同时做多件事情
- 并行:多个人,每人做一个事情
竞态
名词
竞态
:计算结果的正确性与时间有关的现象被称为竞态共享变量
:可以被多个线程共同访问的变量
竞态产生的条件
- 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,导致程序一直进行,无法停止)
- 饥饿(某些线程无法获得其所需资源,而使得任务无法进展)
资源争用与调度
概念
- 一次只能被一个线程占用的资源被称为
排他性资源
- 资源被一个线程访问时,其他线程试图访问该资源的现象被称为
资源争用
。我们要达到的理想状态是:高并发、低争用
资源调度的公平性
资源调度的一个常见特性是:他是否保证公平性(是否先到先得)。
非公平调度策略是我们多数情况下的首选资源调度策略,其优点是吞吐量大,缺点是资源申请者申请资源所需时间的是偏差可能较大,并可能导致饥饿现象。
公平调度适合在资源的持有线程占用资源的时间相对长或资源的平均申请时间间隔相对长的情况下,或对申请的时间偏差有要求的情况下使用,优点和缺点则反之。
Java多线程编程实战02:多线程编程模型的更多相关文章
- Java并发编程实战 01并发编程的Bug源头
摘要 编写正确的并发程序对我来说是一件极其困难的事情,由于知识不足,只知道synchronized这个修饰符进行同步. 本文为学习极客时间:Java并发编程实战 01的总结,文章取图也是来自于该文章 ...
- Java高级项目实战02:客户关系管理系统CRM系统模块分析与介绍
本文承接上一篇:Java高级项目实战之CRM系统01:CRM系统概念和分类.企业项目开发流程 先来CRM系统结构图: 每个模块作用介绍如下: 1.营销管理 营销机会管理:针对企业中客户的质询需求所建立 ...
- Java并发编程实战笔记—— 并发编程1
1.如何创建并运行java线程 创建一个线程可以继承java的Thread类,或者实现Runnabe接口. public class thread { static class MyThread1 e ...
- Java并发编程实战笔记—— 并发编程3
1.实例封闭 class personset{ private final Set<Person> myset = new HashSet<Person>(); public ...
- Java并发编程实战笔记—— 并发编程2
1.ThreadLocal Java中的ThreadLocal类可以让你创建的变量只被同一个线程进行读和写操作.因此,尽管有两个线程同时执行一段相同的代码,而且这段代码又有一个指向同一个ThreadL ...
- Java并发编程实战笔记—— 并发编程4
1.同步容器类 同步容器类都是线程安全的,但在某些情况下可能需要额外的客户端加锁保护复合操作. 容器上常见的复合操作包括但不限于:迭代(反复访问数据,直到遍历完容器中所有的元素为止).跳转(根据指定顺 ...
- Java并发编程实战 02Java如何解决可见性和有序性问题
摘要 在上一篇文章当中,讲到了CPU缓存导致可见性.线程切换导致了原子性.编译优化导致了有序性问题.那么这篇文章就先解决其中的可见性和有序性问题,引出了今天的主角:Java内存模型(面试并发的时候会经 ...
- Java并发编程实战 03互斥锁 解决原子性问题
文章系列 Java并发编程实战 01并发编程的Bug源头 Java并发编程实战 02Java如何解决可见性和有序性问题 摘要 在上一篇文章02Java如何解决可见性和有序性问题当中,我们解决了可见性和 ...
- Java并发编程实战 04死锁了怎么办?
Java并发编程文章系列 Java并发编程实战 01并发编程的Bug源头 Java并发编程实战 02Java如何解决可见性和有序性问题 Java并发编程实战 03互斥锁 解决原子性问题 前提 在第三篇 ...
随机推荐
- 遇到的错误之“Cannot find module 'XXX’的错误”
一.问题: 在进行webpack打包的时候,会出现Cannot find module 'XXX'的错误,找不到某个模块的错误 二.解决方法: (1)方法1: 直接进行npm install重新打包: ...
- (stm32f103学习总结)—stm32中断系统
一.NVIC 介绍 NVIC 英文全称是 Nested Vectored Interrupt Controller,中文意思就是嵌套向量中断控制器,它属于 M3 内核的一个外设,控制着芯片的中断相关功 ...
- 论文阅读总结-Patient clustering improves efficiency of federated machine learning to predict mortality and hospital stay time using distributed electronic medical records
一.论文提出的方法: 使用进入ICU前48h的用药特征作为预测因子预测重症监护患者的死亡率和ICU住院时间. 用到了联邦学习,自编码器,k-means聚类算法,社区检测. 数据集:从50家患者人数超过 ...
- java中Error和Exception用法上有什么区别,Error是怎么回事?
顺便提一句, 和Exception 相对应的,还有Error,Error(错误)表示系统级的错误和程序不必处理的异常,是JRE(java运行环境)的内部错误或者硬件问题,比如,另外 某一处地方的bug ...
- 多态,动态方法调度(dynamic method dispatch)?
8.多态Polymorphism,向上转型Upcasting,动态方法调度(dynamic method dispatch) 什么叫多态?简言之,马 克 - t o - w i n:就是父类引用指向子 ...
- MySQL 中的 SQL 语句详解
@ 目录 总结内容 1. 基本概念 2. SQL列的常用类型 3. DDL简单操作 3.1 数据库操作 3.2 表操作 4. DML操作 4.1 修改操作(UPDATE SET) 4.2 插入操作(I ...
- 初识react中高阶组件
高阶组件并不是一个组件,而是一个函数 这个函数返回值是一个组件,并且接受一个组件做为参数:并且返回一个新组件: function HighOC(WrapComponent){ //定义一个高阶组件 , ...
- Java重载容易引发的错误—返回类型
方法的签名仅仅与方法名和参数类型相关,而与访问控制符.返回类型无关,以及方法体中的内容都没有关系,下面用一个例子说明; 如果Student类两种签名,myStudent(int,int)返回int 类 ...
- Android 环境搭建记录
Android 环境搭建记录 官网 https://developer.android.com/ studio 下载地址 官方下载 jikexueyuanwiki 国内镜像 studio历史版本 安装 ...
- java数组算法——数组元素的赋值2
java数组算法--数组元素的赋值2--java经典面试题:创建一个长度为6的int型数组,要求数组元素的值都在1-30之间,且是随机赋值.同时要求元素时的值各不相同