并发
并发基础
JMM
- 线程通信
- 消息传递
AQS
- AbstractQueuedSynchronizer同步器
- 用来构建锁或者其它同步组件的基础框架,使用int表示同步状态,通过内置的fifo队列完成资源获取线程的排队工作
- getstate
- setstate
- compareAndSetState
- 实现
- 同步队列
- 双链表
- 基于CAS死循环设置尾节点
- 通过获取同步状态成功设置首节点
- 每个节点自旋观察是否获取到同步状态
- 双链表
- 独占同步状态获取与释放
- 获取失败生成节点
- 同步队列
- 用来构建锁或者其它同步组件的基础框架,使用int表示同步状态,通过内置的fifo队列完成资源获取线程的排队工作
- CLH同步队列
- 同步状态获取释放
- 线程阻塞和唤醒
CAS
- compare and swap
原子操作实现原理
- 相关术语
- 缓存行 cache line
- 缓存最小操作单位
- 比较并交换 CAS
- cpu流水线
- 内存顺序冲突
- 假共享引起
- 多个cpu同时修改同一个缓存行的不同部分引起其中一个cpu的操作无效
- cpu必须清空流水线
- 假共享引起
- 缓存行 cache line
- 处理器实现原子操作
- 处理器读取一个字节时,其它处理器不能访问这个字节的内存地址
- 总线锁
- lock信号,一个处理器输入此信号,其它处理器请求将被阻塞,处理器独占共享内存,其他处理器不能操作其它内存地址的数据,开销较大
- 缓存锁
- 内存区域如果被缓存在处理器的缓存行中,执行锁操作写回到内存时,修改内部的内存地址,并允许由它的缓存一致性机制保证操作的原子性,当其他处理写回被锁定的缓存行的数据时,会使缓存行无效
- 缓存一致性机制会阻止同时修改由两个以上处理器缓存的内存区域
- 不适用
- 数据不能缓存在处理器内部或者跨多个缓存行
- 调用总线锁定
- 不支持处理器缓存的机器
- 数据不能缓存在处理器内部或者跨多个缓存行
- 内存区域如果被缓存在处理器的缓存行中,执行锁操作写回到内存时,修改内部的内存地址,并允许由它的缓存一致性机制保证操作的原子性,当其他处理写回被锁定的缓存行的数据时,会使缓存行无效
- 总线锁
- 处理器读取一个字节时,其它处理器不能访问这个字节的内存地址
- jvm实现
- 锁
- CAS
- CMPXCHAG指令
- 问题
- ABA
- atomicStampedRefrence
- compareandSet
- (V expectedReference,
-
V newReference,
-
int expectedStamp,
-
int newStamp)
- compareandSet
- atomicStampedRefrence
- 循环时间长开销大
- pause
- 延迟流水线指令,减少cpu消耗
- 锁
- 合并变量
- 读写锁
- 线程池
- atomicRefrence
- pause
- 只能保证一个共享变量的原子操作
- ABA
对象头
- Mark Word
*- 存储对象的hashcode或者锁信息等
- 对象hashcode
- 对象分代年龄 4
- 是否偏向锁 1
- 锁标志位 0
- 00 轻量级锁
- 指向栈中锁记录的指针
- 01偏向锁
- 线程ID Epoch 分代年龄 1 01
- 10重量级锁
- 指向互斥量的指针
- 11 gc标志
- 01 无锁
- 对象的HashCode 分代年龄 0 01
- 00 轻量级锁
- 存储对象的hashcode或者锁信息等
- class metadata address
- 存储到对象类型数据的指针
- array length
- 数据长度(如果当前对象时数组)
锁升级与对比
- 偏向锁
- 等到竞争才释放锁
- 撤销偏向锁后恢复到未锁定(标志位为“01”)或轻量级锁(标志位为“00”)的状态。
- 等到竞争才释放锁
- 轻量级
- 未获取锁的线程会自旋获取锁,存在竞争膨胀成重量级锁,持有轻量级锁的线程会解锁失败
- 重量级锁
- 未获得锁的线程获取锁时会被阻塞
多线程
- 进程是资源分配的最小单位,线程是CPU调度的最小单位
- 好处
- 更多处理器核心
- 更快相应
- 更好编程模型
- 状态
- NEW
- new了一个实例
- Daemon
- 支持型线程,用作程序中后台调度以及支持性工作
- USER
- Daemon
- new了一个实例
- RUNNABLE
- READY
- 就绪状态只是说你资格运行,调度程序没有挑选到你,你就永远是就绪状态。
- 调用线程的start()方法,此线程进入就绪状态。
- 当前线程sleep()方法结束,其他线程join()结束,等待用户输入完毕,某个线程拿到对象锁,这些线程也将进入就绪状态。
- 当前线程时间片用完了,调用当前线程的yield()方法,当前线程进入就绪状态。
- 锁池里的线程拿到对象锁后,进入就绪状态。
- RUNNING
- 线程调度程序从可运行池中选择一个线程作为当前线程时线程所处的状态。这也是线程进入运行状态的唯一一种方式。
- READY
- BLOCK
- 阻塞,线程阻塞于锁
- 阻塞状态是线程阻塞在进入synchronized关键字修饰的方法或代码块(获取锁)时的状态。
- 阻塞,线程阻塞于锁
- WAITING
- 等待,当前线程需要等待其它线程做出一些特点动作,通知或者中断
- 处于这种状态的线程不会被分配CPU执行时间,它们要等待被显式地唤醒,否则会处于无限期等待的状态。
- 释放对象锁,sleep不会
- 等待,当前线程需要等待其它线程做出一些特点动作,通知或者中断
- TIME_WAITING
- 执行时间自行返回
- TERMINATED
- 方法对比
- Thread.sleep(long millis),一定是当前线程调用此方法,当前线程进入TIMED_WAITING状态,但不释放对象锁,millis后线程自动苏醒进入就绪状态。作用:给其它线程执行机会的最佳方式。
- Thread.yield(),一定是当前线程调用此方法,当前线程放弃获取的CPU时间片,但不释放锁资源,由运行状态变为就绪状态,让OS再次选择线程。作用:让相同优先级的线程轮流执行,但并不保证一定会轮流执行。实际中无法保证yield()达到让步目的,因为让步的线程还有可能被线程调度程序再次选中。Thread.yield()不会导致阻塞。该方法与sleep()类似,只是不能由用户指定暂停多长时间。
- t.join()/t.join(long millis),当前线程里调用其它线程t的join方法,当前线程进入WAITING/TIMED_WAITING状态,当前线程不会释放已经持有的对象锁。线程t执行完毕或者millis时间到,当前线程进入就绪状态。
- obj.wait(),当前线程调用对象的wait()方法,当前线程释放对象锁,进入等待队列。依靠notify()/notifyAll()唤醒或者wait(long timeout) timeout时间到自动唤醒。
- obj.notify()唤醒在此对象监视器上等待的单个线程,使其从wait方法返回,返回的前提是该线程获取到了对象的锁,选择是任意性的。notifyAll()唤醒在此对象监视器上等待的所有线程。
- waitnotify运行过程
- notify会把等待队列中的线程移到同步队列
- waitnotify运行过程
- 原文:https://blog.csdn.net/pange1991/article/details/53860651?utm_source=copy
- NEW
- 线程间通信
- 线程池
- 消除线程的频繁创建消亡带来的开销,面对过量任务平缓劣化
内存模型
volatile
- 保证所有线程看到值一致,对单个volatile变量读写具有原子性 volatile++不具备
- lock前缀
- 当前处理器缓存行数据写会系统内存
- 写回内存的操作会使其它CPU里缓存了该内存地址的数据无效
- 多处理器的情况下,每个处理器通过嗅探在总线上传播的数据,检查自己的缓存是否过期
- 总线(Bus)是计算机各种功能部件之间传送信息的公共通信干线,它是由导线组成的传输线束, 按照计算机所传输的信息种类,计算机的总线可以划分为数据总线、地址总线和控制总线,分别用来传输数据、数据地址和控制信号。
- lock指令执行期间会锁住总线,使得其它处理器无法通过总线访问内存
- lock前缀
锁的内存语义
- 线程获得锁时,JMM会使该线程对应的本地内存置为无效,临界区代码必须从主内存读取共享变量
基础
- 并发编程两个关键问题
- 如何通信、
- 共享内存
- java并发采用模型
- 消息传递
- 共享内存
- 如何同步
- 控制不同线程间操作的相对顺序
- 如何通信、
- 内存模型抽象
- jmm通过控制主内存与每个线程的本地内存之间的交互,来提供内存可见性的保证
- 主内存(共享变量)
- 本地内存A 共享变量的副本
- 线程A
- 本地内存B 共享变量的副本
- 线程B
- 本地内存A 共享变量的副本
- 指令执行
- 指令流水线有取指(IF)、译码(ID)、执行(EX)、访存(MEM)、写回寄存器堆(WB)5个阶段 每个阶段都要花费一个机器周期
- 重排序
- 过程
- 处理器重排序,可以插入内存屏障禁止特定类型的重排序
- 源代码
- 编译器优化
- 指令并行 指令并行技术 ILP
- 内存系统 读/写缓冲区
- 最终执行序列
- 概要: 处理器重排序,可以插入内存屏障禁止特定类型的重排序
- 数据依赖性
- as-is-serial
- 不管怎么排,结果不能变
- 多线程中,对存在控制依赖的重排序,可能会改变程序的执行结果
- 顺序一致性
- 过程
- 内存屏障
- loadload
- loadstore
- storesore
- storeload
- 全能型
- 具备其它三个屏障的效果,开销很昂贵,会把缓冲区中所有数据刷入内存
- 全能型
- Happen-Before
- 一个操作的结果要对另外一个操作可以见,那么这两个操作之间就存在happen before关系,使用于同一线程或者不同线程
final域内存语义
- 构造函数内对一个final域的写入,与随后将这个被构造对象的引用赋值给一个引用变量,不能重排序
- jmm禁止编译器把final域的写重排序到构造函数之外
- final域写之后,构造函数return之前,掺入storestore屏障
- 初次读一个包含final域的对象的引用与随后读,操作不能重排序
- final引用不能从构造函数内溢出
双重检查锁定和延迟初始化
- 开销大的操作延迟加载
- 重排序解决方案
- volatile
- 类初始化解决方案
- 类初始化期间,jvm会去获得一个锁,可以同步多个线程对同一个类的初始化
- 初始化步骤
- 获取初始化锁
- 取得锁的线程A执行类初始化,读取到state=uninitialized,未取得锁的线程BCD…在condition中等待
- 线程A设置类已初始化state=initialized唤醒在condition中等待的所有县城
- B获取初始化锁,读取到state,已初始化,释放锁
- CD…执行
- 初始化步骤
- 类初始化期间,jvm会去获得一个锁,可以同步多个线程对同一个类的初始化
- 重排序解决方案
lock
synchronized(非公平锁)(隐式获取锁,取锁解锁过程固话)
- 分类
- 普通同步方法
- 锁定当前实例对象
- 静态同步方法
- 锁定当前类
- 同步代码块
- 锁synchronize括号里面配置的对象
- 普通同步方法
- 原理 进入和退出monitor
- 代码块 两者配对出现
- monitorenter 被持有后出于锁定状态,执行enter时,尝试获得monitor所有权
- 每个对象有一个监视器锁(monitor)。当monitor被占用时就会处于锁定状态,线程执行monitorenter指令时尝试获取monitor的所有权,过程如下:
- 1、如果monitor的进入数为0,则该线程进入monitor,然后将进入数设置为1,该线程即为monitor的所有者。
- 2、如果线程已经占有该monitor,只是重新进入,则进入monitor的进入数加1.
- 3.如果其他线程已经占用了monitor,则该线程进入阻塞状态,直到monitor的进入数为0,再重新尝试获取monitor的所有权。
- monitorexit
- 执行monitorexit的线程必须是objectref所对应的monitor的所有者。
- 指令执行时,monitor的进入数减1,如果减1后进入数为0,那线程退出monitor,不再是这个monitor的所有者。其他被这个monitor阻塞的线程可以尝试去获取这个 monitor 的所有权。
- 通过这两段描述,我们应该能很清楚的看出Synchronized的实现原理,Synchronized的语义底层是通过一个monitor的对象来完成,其实wait/notify等方法也依赖于monitor对象,这就是为什么只有在同步的块或者方法中才能调用wait/notify等方法,否则会抛出java.lang.IllegalMonitorStateException的异常的原因。
- https://www.cnblogs.com/paddix/p/5367116.html
- monitorenter 被持有后出于锁定状态,执行enter时,尝试获得monitor所有权
- 方法
- 方法的同步并没有通过指令monitorenter和monitorexit来完成(理论上其实也可以通过这两条指令来实现),不过相对于普通方法,其常量池中多了ACC_SYNCHRONIZED标示符。JVM就是根据该标示符来实现方法的同步的:当方法调用时,调用指令将会检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置,如果设置了,执行线程将先获取monitor,获取成功之后才能执行方法体,方法执行完后再释放monitor。在方法执行期间,其他任何线程都无法再获得同一个monitor对象。 其实本质上没有区别,只是方法的同步是一种隐式的方式来实现,无需通过字节码来完成。
- 对象,监视器,同步队列,执行线程关系
- 任意线程访问被synchronized保护的对象访问步骤
- 获取object的监视器
- 获取失败进入同步队列,线程状态变为block
- 获得锁的线程释放锁,唤醒block线程尝试获取监视器
- 任意线程访问被synchronized保护的对象访问步骤
- 代码块 两者配对出现
lock接口
- ReentrantLock
- 分类
- 公平锁
- 公平锁就是在获取锁之前会先判断等待队列是否为空或者自己是否位于队列头部,该条件通过才能继续获取锁
- 保证fifo进行大亮线程切换
- 公平锁就是在获取锁之前会先判断等待队列是否为空或者自己是否位于队列头部,该条件通过才能继续获取锁
- 非公平锁
- 保证吞吐量,可能会存在饥饿,极少线程切换
- 公平锁
- 实现
- 通过组合自定义同步器实现锁的获取和释放,同一个获取锁的线程可以再次进入,
- 分类
- ReentrantReadWriteLock
- 新增
- 公平性选择
- 重进入
- 锁降级,写锁能够降级成读锁
- 持有写锁,获取读锁,释放写锁
- 实现
- 按位切割使用变量,高16读状态,低16写状态
- 读锁获取需要等其他线程写锁释放,写锁被获取,其他读写线程的后续访问将被阻塞
- 新增
- 新特性
- 尝试非阻塞的获取锁
- 能被中断的获取锁
- 超时获取锁
- 主要方法
内存语义
内存模型
locksuppport
- 阻塞或者唤醒线程,使用locksupport完成相应工作
condition()
- 与lock配合实现等待通知模式
- 子主题 1
- 等待
- 调用addConditionWaiter将当前线程加入等待队列;
- 调用fullRelease释放当前线程节点的同步状态,唤醒后继节点;
- 线程进入等待状态;
- 线程被唤醒后,从while循环中退出,调用acquireQueued尝试获取同步状态;
- 同步状态获取成功后,线程从await方法返回。
- 作者:miaoLoveCode
- 链接:https://www.jianshu.com/p/be2dc7c878dc
- 唤醒
- step1:前置检查,判断当前线程是否是获取了锁的线程,如果不是抛出异常IllegalMonitorStateException,否则,执行step2;
- step2:取得等待队列的头结点,头结点不为空执行doSignal,否则,signal结束。
- 整个doSignal完成了这两个操作:调用transferForSignal将节点从等待队列移动到同步队列,并且,将该节点从等待队列删除。
- step1:将节点waitStatus设置为0,设置成功执行step2,否则返回false;
- step2:调用enq方法将该节点加入同步队列;
- step3:使用LockSuppor.unpark()方法唤醒该节点的线程。
- 整个doSignal完成了这两个操作:调用transferForSignal将节点从等待队列移动到同步队列,并且,将该节点从等待队列删除。
死锁
其它
原子操作
- 基本类型
- AtomicInteger
- AtomicLong
- AtomicBoolean
- 数组
- AtomicIntegerArray
- AtomicLongArray
- AtomicReferenceArray
- 引用类型
- AtomicReference
- AtomicReferenceFieldUpdater
- 其它
- threadlocal
- 对象为建,任意对象为值,结构被附带在线程上,一个线程可以根据一个threadlocal对象查询到绑定在这个线程上的值,类维护了一个以当前线程为key的键值对
- fork/join
- 用于并行执行任务的框架,是一个把大任务分割成若干个小任务,最终汇总每个小任务结果后得到大任务结果的框架
- 工作窃取算法
- forkJoinPool
- forkjointask数组
- forkjoinworkerThread数组
- 用于并行执行任务的框架,是一个把大任务分割成若干个小任务,最终汇总每个小任务结果后得到大任务结果的框架
- threadlocal
并发集合
- ConcurrentHashMap
- 其中抛弃了原有的 Segment 分段锁,而采用了 CAS + synchronized 来保证并发安全性。
- 源码解析 https://mp.weixin.qq.com/s/fZRPogkkUfBnhbZQB5r-uw
- LinkedBlockingQueue
- ReentrantLock lock
- concurrentLinkedQueue
- 基于链接节点的无界线程安全队列
- offer
- 将指定元素插入此队列的尾部。
- poll
- 获取并移除此队列的头,如果此队列为空,则返回 null。
- peek
- 获取但不移除
- offer
- 基于链接节点的无界线程安全队列
线程池
- Executor
- 示意图
- 子主题 1
- 组成
- ThreadPoolExcutor
- 示意图
- ThreadPoolExecutor
- 类型
- fixedThreadPool
- singleThreadPoolExcute
- cacheThreadPOOL
- corePool
- 核心线程池大小
- maximumPool
- 最大线程池大小
- BlockingQueue
- 暂时保存任务的工作队列
- RejectedExecutionHandler
- 线程池饱和时,execute调用方法
- excute
- 类型
- Future Callable
- ScheduledExecutorService
- 线程池架构
- 子主题 1
- 子主题 1
- 如果线程池中的线程数量少于corePoolSize,就创建新的线程来执行新添加的任务
- 如果线程池中的线程数量大于等于corePoolSize,但队列workQueue未满,则将新添加的任务放到workQueue中
- 如果线程池中的线程数量大于等于corePoolSize,且队列workQueue已满,但线程池中的线程数量小于maximumPoolSize,则会创建新的线程来处理被添加的任务
- 如果线程池中的线程数量等于了maximumPoolSize,就用RejectedExecutionHandler来执行拒绝策略
- 子主题 1
- 子主题 1
工具类
- CyclicBarrier
- 一组线程到达一个屏障被阻塞,与countdown相比,可以重置
- CountDownLatch
- 一个或者多个线程等待其它线程完成操作
- Semaphore
- 控制访问特定资源的线程数量
- exchanger
- 进行线程间的数据交换
减少上下文切换
- 无锁并发编程
- CAS
- 使用较少线程
- 使用协程
- 英文Coroutines,是一种比线程更加轻量级的存在。正如一个进程可以拥有多个线程一样,一个线程也可以拥有多个协程。最重要的是,协程不是被操作系统内核所管理,而完全是由程序所控制(也就是在用户态执行)。