并发编程的挑战
多线程一定快吗
上下文切换
上下文切换次数和时长
如何减少上下文切换
死锁
资源限制的挑战
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(线程池最大数量)
注意,如果使用无界的任务队列,这个参数就没有意义了, 建议指定线程最大值,每个线程在分配时候的大小默认是512KThreadFactory
用于创建线程的工厂,可以通过线程工厂给每个创建出来的线程设置更有意义的名字,方便问题定位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