Note_java并发编程的艺术

  1. 并发编程的挑战
  2. Java并发机制的底层实现原理
    1. volatile的应用
      1. cpu术语
      2. lock前缀指令
      3. volatile优化
    2. synchronnized的实现原理与应用
      1. jdk1.6的改进
      2. 实现同步的基础
      3. 对象头
        1. 32bit
        2. 64bit
      4. 锁的升级和对比
    3. 原子操作的实现原理
      1. 术语
      2. 处理器
        1. 使用总线锁保证原子性
        2. 使用缓存锁定来保证原子性
      3. Java
        1. CAS
  3. Java内存模型
    1. Java内存模型的基础
      1. 并发编程模型的两个关键问题
      2. 线程之间的通信机制
      3. Java内存模型的抽象结构
      4. 并发编程模型的分类
        1. 写缓冲区(写读的顺序)
        2. 内存屏障
      5. happens-before简介
    2. 重排序(为了提高并行度)
      1. 数据依赖性
      2. as-if-serial语义
      3. 程序顺序规则
      4. 对多线程的影响
    3. 顺序一致性
      1. 数据竞争
      2. 顺序一致性内存模型
      3. 同步程序的顺序一致性效果
      4. 未同步程序的执行特性
    4. volatile的内存语义
      1. 特征
      2. 建立的happens-before关系
      3. volatile写-读的内存语义
      4. 实现
    5. 锁的内存语义
      1. CAS
      2. concurrent包的实现
    6. final域的内存语义
      1. final域的重排序规则
      2. 写final域的重排序规则
      3. 读final域的重排序规则
      4. final引用不能从构造函数内‘溢出’
      5. 语义
    7. happens-before
      1. JMM的设计
      2. 定义
      3. 规则
    8. 双重检查锁定与延迟初始化
      1. 错误的根源
      2. 解决方式
      3. 类对初始化
    9. Java内存模型综述
      1. 处理器的内存模型
      2. 各种内存模型之间的关系
      3. JMM的内存可见性保证
      4. JSR-133对内存语义的修复
  4. Java并发编程基础
    1. 线程简介
    2. 启动和终止线程
    3. 线程间通信
      1. 通知/等待
      2. 等待方
      3. 通知方
  5. Java中的锁
    1. Lock
    2. AQS
    3. 与锁的关系
    4. 同步队列
    5. ReenLock
  6. Java中的线程池
    1. 为什么使用线程池
    2. 实现原理
    3. 使用
      1. 参数
      2. 提交任务方式
      3. 关闭线程池
      4. 合理使用
        1. 任务性质
        2. 任务优先级
        3. 任务执行时间
        4. 任务的依赖性
        5. 监控

image-20190606190506267

并发编程的挑战

多线程一定快吗

上下文切换
上下文切换次数和时长
如何减少上下文切换

死锁

资源限制的挑战

Java并发机制的底层实现原理

volatile的应用

cpu术语

​ 内存屏障

lock前缀指令

​ 缓存写回内存
​ 其他处理器的缓存无效

volatile优化

​ 队列集合类LinkedTransferQueue、队列64位

synchronnized的实现原理与应用

jdk1.6的改进

​ 偏向锁
​ 轻量级锁
​ 锁的存储结构
​ 升级过程

实现同步的基础

​ 普通同步方法,锁是当前实例对象
​ 静态同步方法,锁是当前类的Class对象
​ 同步方法块,锁是括号里配置的对象

对象头

32bit

​ 锁状态
​ 25bit 对象的hascode
​ 4bit 对象的分代年龄
​ 1bit 是否偏向锁
​ 2bit 锁标志位

64bit

锁的升级和对比

​ 偏向锁
​ 偏向锁标识
​ 轻量级锁
​ CAS、自旋
​ 重量级锁
​ 追求吞吐

原子操作的实现原理

术语

​ 比较并交换
​ CPU流水线
​ 将一条执行分成5-6步再由这些电路单元分别执行
​ 内存顺序冲突

处理器

使用总线锁保证原子性

​ 把CPU和内存之间的通信都锁住了

使用缓存锁定来保证原子性

​ 某个内存地址的操作是原子性即可
失效场景
​ 操作数据不能备缓存在处理器内存,或操作的数据跨多个缓存行
​ 有些处理器不支持

Java

CAS

​ 处理器提供的CMPXCHG指令
​ 存在的问题
​ ABA问题
​ 循环时间开销大
​ 只能保证一个共享变量的原子操作

Java内存模型

Java内存模型的基础

并发编程模型的两个关键问题

  • 如何通信—何种机制来交换信息
  • 何时同步—控制发生相对顺序的机制

线程之间的通信机制

  • 共享内存—线程之间共享程序的公共状态,通过读-写内存中的公共状态进行隐式通信
  • 消息传递—线程之间没有公共状态,线程之间必须通过发生消息来显式进行通信

Java内存模型的抽象结构

JMM定义了线程和主内存之间的抽象关系,从源代码到指令序列的重排序,处理器通过内存屏障保证顺序

  • 编译器优化的重排序
    编译器在不改变单线程程序予以的前提下,可以重新安排语句的执行顺序

  • 指令级并行的重排序
    现代处理器采用了指令级并行技术来将多条指令重叠执行

  • 内存系统的重排序

    缓存和读/写缓冲区

并发编程模型的分类

写缓冲区(写读的顺序)

写缓冲区可以保证指令流水线持续运行,它可以避免由于处理器停顿下来等待向内存写入数据而产生的延迟
通过以批处理的方式刷新写缓冲区,以及合并写缓存区中对一内存地址的多次写,减少内存总线的占用
存在的问题
写缓存区仅对自己的处理器可见,它会导致处理器执行内存操作的顺序可能会与内存实际操作顺序不一致

内存屏障
  • LoadLoad
  • StoreStore
  • LoadStore
  • StoreLoad
    确保了Store的数据对其他处理器变得可见(指刷新到内存)

happens-before简介

阐述操作之间的内存可见性
两个操作之间存在happens- before关系,并不意味着前一个操作必须在后一个操作之前执行

重排序(为了提高并行度)

数据依赖性

​ 编译器和处理器不会改变存在数据依赖关系的两个操作的执行顺序,前提是单个处理器执行的指令续集和单个线程中执行的操作

as-if-serial语义

​ 不管怎么重排序,程序执行结果不能改变(单线程)

程序顺序规则

对多线程的影响

顺序一致性

数据竞争

​ 在一个线程中写一个变量
​ 在另一个线程读同一个变量
​ 而且写和读没有通过同步来排序

顺序一致性内存模型

​ 特征
​ 一个线程中所有操作必须按照程序的顺序来执行
​ 所有线程都只能看到一个单一的操作执行顺序,每个操作都必须原子执行且立即对所有线程所见
​ 任意时间点最多只能有一个线程可以连接到内存,串行化

同步程序的顺序一致性效果

​ JMM临界区内的代码可以重排序

未同步程序的执行特性

​ JMM只提供最小安全性,线程执行时读取到的值,要么时之前某个线程写入的值,要么时默认值
​ 差异
​ JMM不保证对64位的long和double型的变量具有原子性,而顺序一致模型保证所有内存读/写都具有原子性

volatile的内存语义

特征

​ 可见性
​ 原子性
​ 复合操作不支持,支持64 long/double

建立的happens-before关系

volatile写-读的内存语义

​ 当写一个volatile变量时,JMM会把该线程对应的本地内存中的共享变量刷新到主内存
​ 当读一个volatile变量时,JMM会把该线程对应的本地内存置为无效,从主内存中读取共享变量

实现

​ 编译器
​ 生成字节码时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序,采用保守的方式
​ 在volatile读后插入内存屏障
​ LoadLoad
​ LoadStore
​ 在volatile写前后插入内存屏障
​ StoreStore 前
​ StoreLoad 后
​ 也可以读前插入StoreLoad,但考虑读多写少情况
​ 优化
​ 省略不必要的屏障
​ 不同处理器,入x86仅会对写-读操作做重排序

旧模型不允许volatile变量之间重排序,但旧的Java模型允许volatile变量与普通变量重排序

锁的内存语义

锁是java并发变成中最重要的同步机制,除了让临界区互斥执行,还可以让释放锁的线程向获取同一个锁的线程发送消息
释放锁时,会把线程对应的本地内存中的共享变量刷新到主内存中
获取锁时,会把线程对应的本地内存置为无效

CAS

​ 同时具有volatile读和写的内存语义
​ 程序根据处理器是否为cmpxchg指令添加lock前缀
​ intel对lock前缀的解释
​ 缓存锁定
​ 禁止指令重排
​ 把写缓冲区中的所有数据刷新到内存

concurrent包的实现

​ 通用化模式
​ 声明共享变量
​ 使用CAS的条件更新来实现线程之间的同步
​ 配合以volatile读/写 和 CAS所具有的volatile读/写 语义来实现线程之间的通信

final域的内存语义

final域的重排序规则

​ 在构造函数内对一个final域的写入,与随后把这个被构造对象的引用赋值给一个引用变量,这两个操作之间不能重排序
​ 初次读一个包含final域的对象的引用,与随后初次读取这个final域,这两个操作之间不能重排序

写final域的重排序规则

​ JMM禁止编译器把final域的写重排序到构造函数之外

读final域的重排序规则

​ 在读一个对象的final域之前,一定先读包含这个final域的对象引用

final引用不能从构造函数内‘溢出’

语义

​ 写final域的重排序规则会要求编译器在final域写之后,构造函数return之前插入一个StoreStore屏障
​ 读final域的重排序规则要求编译器在读final域的操作前面插入一个LoadLoad

happens-before

JMM的设计

​ 两个关键因素
​ 程序员对内存模型对使用
​ 编译器和处理器对内存模型对实现
​ happens-before要求禁止对重排序
​ 会改变程序执行结果对重排序
​ JMM禁止
​ 不会改变程序执行结果的重排序
​ JMM不禁止

定义

​ as-if-serial语义保证单线程的执行结果不被改变,happens-before关系保证正确同步的多线程执行结果不被改变

规则

​ 程序顺序规则
​ 监视器锁规则
​ volatile变量规则
​ 传递性
​ start规则
​ join规则

双重检查锁定与延迟初始化

错误的根源

​ 1.分配对象的内存空间
​ 2.初始化对象
​ 3.设置instance执行分配的内存地址
​ 重排序 2 和 3,获取一个未初始化的对象

解决方式

​ 不允许2和3重排序
​ volatile
​ 允许2和3重排序,但不允许其他线程‘看到’这个重排序
​ 在执行类初始化期间,JVM会去获取一个锁,这个锁可以同步多个线程对同一个类对初始化

类对初始化

​ 一个类或接口类型立即被初始化对情况
​ T是一个类,T对示例被创建
​ T是一个类,且T中声明对一个静态方法被调用
​ T中声明对一个静态字段被赋值
​ T中声明对一个静态字段被使用,而且这个字段不是一个常量字段
​ T是一个顶级类,而且一个断言语句嵌套在T内部被执行

Java内存模型综述

处理器的内存模型

​ JMM屏蔽了不同处理器内存模型的差异

各种内存模型之间的关系

​ 处理器内存模型是硬件级的内存模型
​ JMM语言级内存模型
​ 顺序一致性内存模型是一个理论参考模型

JMM的内存可见性保证

JSR-133对内存语义的修复

Java并发编程基础

线程简介

启动和终止线程

线程间通信

通知/等待

​ 监视器
​ 对象
​ 等待队列
​ 同步队列

等待方

​ 获取对象锁
​ 如果条件不满足,那么调用对象的wait()方法,被通知后仍要检查条件
​ 条件满足则执行对应逻辑

通知方

​ 获得对象的锁
​ 改变条件
​ 通知所有等待在对象上的线程

Java中的锁

Lock

AQS

​ 主要使用方式是继承,推荐被定义为自定义同步组件的静态内部类

与锁的关系

​ 锁是面向使用者,它定义了使用者与锁交互的接口,隐藏了实现细节
​ 同步器面向的是锁的实现者,它简化了锁的实现方式,屏蔽了同步状态管理,线程的排队,等待与唤醒等底层操作

同步队列

​ 节点
​ 是什么
​ 当前线程获取同步状态失败时,同步器会将当前线程以及等待状态等信息构成一个节点,当同步状态释放时,会把首节点唤醒,再次尝试获取同步状态
​ 属性
​ 等待状态
​ 前驱节点
​ 后继节点
​ 等待队列中的后继节点
​ 获取同步状态的线程
​ FIFO双向队列
​ 通过CAS设置尾节点

ReenLock

Java中的线程池

为什么使用线程池

​ 降低资源消耗
​ 通过重复利用已创建的线程降低线程创建和销毁造成的消耗
​ 提高响应速度
​ 当任务到达时,任务可以不需要等到线程创建就能立即执行
​ 提高线程的可管理性
​ 线程是稀有资源,使用线程池可以进行统一分配、调优和监控

实现原理

​ 线程池处理流程
​ 1.首先,判断核心线程池里的线程是否都在执行任务,如果不是,创建任务,否则进入下个流程
​ 2.判断工作队列是否已经满了,如果不是,存储在工作队列,否则进入下个流程
​ 3.判断线程池的线程是否都处于工作状态,如果没有,创建一个新的线程执行任务(需要全局锁),否则交给饱和策略来处理
​ 为什么
​ 尽可能地避免获取全局锁,因为从处理流程来看,在完成预热后,几乎执行步骤都是落在2上

使用

参数

  • corePoolSize(线程池的基本大小)
    提交一个任务到线程池时,线程池会创建一个任务,即使其他空闲到基本线程能够执行新任务,等需要执行的任务数大于线程池基本大小时就不再创建
    可以提前调用prestartAllCoreThreads提前创建并启动所有基本线程

  • runnableTaskQueue(任务队列)
    ArrayBlockingQueue
    基于数组结构的有界阻塞队列,FIFO
    LinkedBlockingQueue
    基于链表的阻塞队列,FIFO
    SynchronousQueue
    不存储元素的阻塞队列(使用场景是防止服务器重启或者down了 丢失队列中的数据)
    PriorityBlockQueue
    具有优先级的无限阻塞队列
    注意 , 建议使用有界队列

  • maximumPoolSize(线程池最大数量)
    注意,如果使用无界的任务队列,这个参数就没有意义了, 建议指定线程最大值,每个线程在分配时候的大小默认是512K

  • ThreadFactory
    用于创建线程的工厂,可以通过线程工厂给每个创建出来的线程设置更有意义的名字,方便问题定位

  • RejectedExecutionHandler(饱和策略)
    AbortPolicy(默认)
    直接抛出异常
    CallerRunsPolicy
    只用调用者所在线程来运行任务
    DiscardOldestPolicy
    丢弃队列里最近的一个任务,并执行当前任务
    DiscardPolicy
    丢弃不处理
    注意 也可以实现RejectExecutionHandler接口自定义策略,一般比较重要的,可以持久化到文件,或者另外的队列

  • keepAliveTime(线程活动保持时间)
    线程池的工作线程空闲后,保持存活的时间
    TimeUnit(单位)

提交任务方式

  • execute 不带返回结果
  • submit 携带返回结果

关闭线程池

  • shutdown

  • shutdownNow

    都是设置中断标记, 两者的差异只是在于shutdownNow不会执行等待队列中的任务

合理使用

任务性质

​ CPU密集型
​ Cpu+1
​ IO密集型
​ 2*Cpu

任务优先级

​ 优先级队列

任务执行时间

​ 使用优先级队列,让时间短的任务先执行
​ 交给不同规模的线程池处理

任务的依赖性

​ 依赖数据库链接池的任务,因为提交需要等待数据库返回结果,等待时间越长,空闲时间越长,所以设置线程数应该尽量大,同时注意针对这种昂贵的资源,记得使用超时机制

监控

​ largestPoolSize-最大线程数
​ getPoolSize-线程池的线程数量
​ getActiveCount-获取活动的线程数

​ 扩展,继承线程池


转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 951488791@qq.com

文章标题:Note_java并发编程的艺术

字数:3.8k

本文作者:zhengyumin

发布时间:2019-06-06, 19:05:01

最后更新:2019-08-31, 18:30:30

原始链接:http://zyumin.github.io/2019/06/06/Note_java%E5%B9%B6%E5%8F%91%E7%BC%96%E7%A8%8B%E7%9A%84%E8%89%BA%E6%9C%AF/

版权声明: "署名-非商用-相同方式共享 4.0" 转载请保留原文链接及作者。