Java并发编程实战脑图
2020-07-15 13:45:41 31 举报
AI智能生成
Java并发 脑图
作者其他创作
大纲/内容
10. 避免活跃性危险
死锁的分类
锁顺序死锁
方法A先获取Q对象的锁再获取W对象的锁
方法B先获取W对象的锁再获取Q对象的锁
当同时调用A和B时可能会出现死锁
原因:获取锁的顺序不同
方法B先获取W对象的锁再获取Q对象的锁
当同时调用A和B时可能会出现死锁
原因:获取锁的顺序不同
动态的锁顺序死锁
方法先获取参数A的锁,再获取参数B的锁
并发调用此方法,某次调用方法时传参a, b 另一次调用方法时传参b, a(注意传参顺序)
结果两次调用发生死锁(表面上获取锁的顺序相同,实际上获取锁的顺序不同)
并发调用此方法,某次调用方法时传参a, b 另一次调用方法时传参b, a(注意传参顺序)
结果两次调用发生死锁(表面上获取锁的顺序相同,实际上获取锁的顺序不同)
在协作对象之间发生的死锁
A对象调用方法,先获取A对象的锁,再获取B对象的锁
B对象调用方法,先获取B对象的锁,再获取A对象的锁
同时调用两个对象的方法,出现死锁
原因:在对象持有锁的情况下调用了外部对象的方法。当对象被发布到很多地方时出现死锁的地方会更难找到
B对象调用方法,先获取B对象的锁,再获取A对象的锁
同时调用两个对象的方法,出现死锁
原因:在对象持有锁的情况下调用了外部对象的方法。当对象被发布到很多地方时出现死锁的地方会更难找到
开放调用
尽可能在调用方法时不持有锁,如果必须持有锁也不要使用同步方法,尽可能缩小锁的范围
死锁的避免和诊断
支持定时的锁
内置锁不支持定时,显式锁(Lock的tryLock方法可以定时)
通过线程转储信息来分析死锁
JVM利用线程转储记录了线程的栈追踪信息
每个线程有哪些锁,在哪些栈帧中获得这些锁,被阻塞的线程正在等待获取哪些锁
这些信息可以通过IDE获取
每个线程有哪些锁,在哪些栈帧中获得这些锁,被阻塞的线程正在等待获取哪些锁
这些信息可以通过IDE获取
IDEA获取某一时刻的线程转储信息
其他的活跃性危险
饥饿
线程无法访问到它所需要的资源而不能执行
活锁
线程不断重复执行相同的操作
例如:线程执行一个注定失败的任务,失败后任务重新放到阻塞队列中,尝试多次执行以获取成功的响应;
例如:线程A要获取锁1,线程B也要获取锁1。线程A让出资源希望线程B获取到锁,线程B做同样的事。结果双方都获取不到锁
例如:线程执行一个注定失败的任务,失败后任务重新放到阻塞队列中,尝试多次执行以获取成功的响应;
例如:线程A要获取锁1,线程B也要获取锁1。线程A让出资源希望线程B获取到锁,线程B做同样的事。结果双方都获取不到锁
解决方案:给线程添加一些随机性,防止出现多个线程同时取锁又同时放弃锁的情况
小结
1. 尽量让线程获取锁时采用一致的顺序
2. 尽量使用开放调用,减少同时持有多个锁的地方
1. 尽量让线程获取锁时采用一致的顺序
2. 尽量使用开放调用,减少同时持有多个锁的地方
11. 性能与可伸缩性
可伸缩性是指:当增加计算资源时,程序的吞吐量或处理能力相应地增加
使用多线程可以有效同时利用多个处理器的计算能力。但是程序并非只有并发部分,还要有串行部分
Amdahl定律描述的是,在增加计算资源的情况下,程序理论上能实现的最高加速比
(N:处理器个数,F:程序中必须使用串行方式执行的部分)
(N:处理器个数,F:程序中必须使用串行方式执行的部分)
线程引入的开销
上下文切换
内存同步
如:使用了volatile关键字的变量,其值改变时会立即被强行刷新到本地内存
内存同步会造成消耗,所以JVM会自动将一些没有竞争的锁去掉,或使用锁粒度粗化操作将锁的多次上锁和释放合并为一次上锁和释放
例如:同步监视器为new Object()。 方法内多次调用局部变量的使用了同步机制方法(然而这个局部变量并不需要使用同步机制也可以安全完成操作)
例如:同步监视器为new Object()。 方法内多次调用局部变量的使用了同步机制方法(然而这个局部变量并不需要使用同步机制也可以安全完成操作)
阻塞
自旋(循环执行获取锁的操作,直到获取到锁)
操作系统挂起被阻塞的线程
减少锁的竞争(减少串行部分堆可伸缩性的影响)
减少锁的持有时间;减少锁的申请频率;使用有协调机制的独立锁
减少锁的持有时间
合理缩小锁的范围
如果可以的话,尽可能将程序中耗时多的,有阻塞的部分移除锁的范围
例:将日志功能抽取出来,日志的持久化等耗时的工作应该由日志系统在另一个后台线程上执行。
在请求的接口中,日志操作应该被放在并行部分,且只做将日志消息入队的操作,而不进行耗时较长的持久化操作
在请求的接口中,日志操作应该被放在并行部分,且只做将日志消息入队的操作,而不进行耗时较长的持久化操作
减少单个锁的申请频率
锁分解
使用多个锁锁资源,降低使用单个锁时锁竞争激烈的情况。
锁分段
ConcurrentHashMap使用了一个包含16个锁的数组,每个锁保护散列桶的1/16。
锁分段使得最多可以有16个线程同时对一个ConcurrentHashMap执行写操作
锁分段使得最多可以有16个线程同时对一个ConcurrentHashMap执行写操作
缺点:管理锁会带来消耗
避免热点域
例:为记录ConcurrentHashMap中元素的个数,需要使用一个计数器
然而每次增删元素都要访问这个计数器(这个计数器变成了热点域)。
所以ConcurrentHashMap使用多个计数器,每个计数器管理散列桶的一部分。降低了访问单个锁的频率
然而每次增删元素都要访问这个计数器(这个计数器变成了热点域)。
所以ConcurrentHashMap使用多个计数器,每个计数器管理散列桶的一部分。降低了访问单个锁的频率
使用有协调机制的独立锁
例:ReadWriteLock。读写锁,有读锁和写锁两个锁。提高并发性能
12. 并发程序的测试
正确性测试
基本的单元测试
对阻塞操作的测试
安全性测试
资源管理的测试
产生更多的交替操作
使用Thread.yield()主动提高切换上下文的频率
为减少Thread.yield()对源代码的入侵,可以使用aop技术将Thread.yield隐藏起来
性能测试
响应性衡量
添加计时功能,测试程序执行用时
避免性能测试的陷阱
垃圾回收
垃圾回收会影响性能测试
动态编译
当类是被第一次加载时,JVM会先使用解释字节码的方式执行它。
如果一个方法的执行次数足够多,动态编译器会将它编译为机器代码
代码执行方式从解释执行变为直接执行,直接执行的效率更高
而线上程序往往都是直接执行方式执行代码,进行速度测试时如果使用的是解释执行将会使测试毫无意义
如果一个方法的执行次数足够多,动态编译器会将它编译为机器代码
代码执行方式从解释执行变为直接执行,直接执行的效率更高
而线上程序往往都是直接执行方式执行代码,进行速度测试时如果使用的是解释执行将会使测试毫无意义
JVM何时会执行这种编译是无法预测的
对代码路径的不真实采样
JVM会根据程序中的信息对已编译的代码进行优化
同一个方法M在不同环境下可能会被编译为不同的代码。例如单线程下M会被优化,将M放在多线程环境下后,这些优化将不在存在。
所以测试代码应该尽量处于接近生产环境的状态
同一个方法M在不同环境下可能会被编译为不同的代码。例如单线程下M会被优化,将M放在多线程环境下后,这些优化将不在存在。
所以测试代码应该尽量处于接近生产环境的状态
不真实的竞争程度
测试代码中往往给线程分配的任务会访问到共享数据且任务会被快速执行完毕,导致共享数据的锁的竞争很激烈
然而生产环境下给线程分配的任务不会那么快执行完,锁的竞争也没那么激烈
所以测试代码如果给线程分配的任务耗时过于短暂,会造成锁竞争激烈的假象且性能更多被耗费在线程的调度上
然而生产环境下给线程分配的任务不会那么快执行完,锁的竞争也没那么激烈
所以测试代码如果给线程分配的任务耗时过于短暂,会造成锁竞争激烈的假象且性能更多被耗费在线程的调度上
无用代码的消除
优化编译器会找出并消除那些不会对输出结果造成影响的“无用的代码”。所以基准测试中一部分没有做计算的代码可能会被消除,从而产生虚假的测试结果。
解决方案:可以执行计算“无用的对象”散列值并输出一些对于输出结果无关紧要的信息,表示这些代码是有用的,不希望被优化器擦除掉
其他测试方法
代码审查
静态分析工具
如:FindBugs
面向切面(AOP)的测试技术
分析与监视工具
13. 显式锁
Lock与ReentrantLock
轮询锁与定时锁
tryLock()和tryLock(long time, TimeUnit unit)
如果能获取锁就获取锁并返回true,如果不能获取到锁就返回false。不会一直阻塞等待获取锁
如果能获取锁就获取锁并返回true,如果不能获取到锁就返回false。不会一直阻塞等待获取锁
可中断的锁获取操作
tryLock(long time, TimeUnit unit)和lockInterruptibly()都是可中断的
性能考虑因素
JDK6对synchronized进行的修改,使其加速比与ReentrantLock接近。二者相同环境下可实现的吞吐量相近
但ReentrantLock还是略优于synchronized
但ReentrantLock还是略优于synchronized
公平性
公平的锁
等待锁的请求先到先得锁
非公平的锁(多数锁默认是非公平锁,可通过构造函数修改)
等待锁的请求可以“插队”获取锁
当有持有锁的时间很短的请求时非公平的锁性能优于公平的锁
在synchronized和ReentrantLock之间的选择
本书推荐优先使用synchronized
读写锁
ReadWriteLock(接口)
ReentrantReadWriteLock(实现类)
14. 构建自定义的同步工具
状态依赖性的管理
举例:向有界数组中添加数据,只有数组中还有空间时才能添加成功;删除数组中指定的数据,只有数据存在时才能删除成功
对这些前置条件的管理,就是状态依赖性的管理
对这些前置条件的管理,就是状态依赖性的管理
管理方式(不满足前置条件时,程序会做什么事)
return true或false
抛异常
自旋阻塞,直到能成功执行/前置条件为真
不睡眠的自旋(消耗性能)
睡眠的自旋
条件队列
执行wait()方法后,线程将被阻塞,锁也会被释放。线程进入条件队列
条件队列中的线程等待被notify()或notifyAll()唤醒
使用条件队列
条件谓词
过早唤醒
通知
执行notify()或notifyAll()的方法时必须持有条件队列上某个对象的锁
线程被notify()或notifyAll()唤醒后,会尝试获取锁并退出wait()方法
显式的Condition对象
Conditional继承Lock的公平性,对于公平的锁,线程会按照FIFO的顺序从Conditional.await()中释放
AbstractQueueSynchronizer(AQS)
java.util.concurrent中的许多类都是根据这个类构建的。
如果你想构建一个同步器可以尝试使用它作为父类
不过一般不推荐自己实现以一个同步器,使用类库提供的线程的同步器足以满足需求
如果你想构建一个同步器可以尝试使用它作为父类
不过一般不推荐自己实现以一个同步器,使用类库提供的线程的同步器足以满足需求
成员变量
private volatile int state;
private volatile int state;
用于表示同步器的状态
其父类的成员变量
private transient Thread exclusiveOwnerThread;
private transient Thread exclusiveOwnerThread;
用于表示当前占用同步器的线程
tryAcquire()
尝试获取
tryAcquireShared()
以共享方式尝试获取
读写锁的读锁用到了它
读写锁的读锁用到了它
tryAcquireNanos()
以独占方式尝试获取
读写锁的写锁用到了它
读写锁的写锁用到了它
...
java.util.concurrent同步器类中的AQS
ReentrantLock
Semaphore与CountDownLatch
ReentrantReadWriteLock
15. 原子变量与非阻塞同步机制
非阻塞算法用底层的原子机器指令代替锁来确保数据在并发访问中的一致性
可以实现多线程竞争相同的数据时不会发生阻塞,且不会发生死锁和其他活跃性问题
可以实现多线程竞争相同的数据时不会发生阻塞,且不会发生死锁和其他活跃性问题
锁的劣势
一个线程持有锁时,其他线程只能阻塞。
且阻塞时间过长的这些线程执行的任务可能会变得没有意义
且阻塞时间过长的这些线程执行的任务可能会变得没有意义
现代处理器几乎都包含了某种形式的原子读-改-写指令,Java5.0之后可以直接使用这些指令
例如:比较并交换(CAS),关联加载/条件存储
比较并交换(CAS)指令
采用乐观锁机制
多线程使用CAS方式修改同一个变量时,只有一个线程能成功,其他线程都将失败
但是失败的线程不会被挂起,而是直接被告知修改失败
但是失败的线程不会被挂起,而是直接被告知修改失败
CAS具有很好的可伸缩性
所以使用CAS的类也具有很好的可伸缩性
所以使用CAS的类也具有很好的可伸缩性
原子变量类使用了CAS
ABA问题
问题描述:V的值由A变为B,又变为A。但是执行CAS时我希望V的值是最初的A而不是最后的A
解决方案:使用版本号
举例:zookeeper 的每个节点值都赋有版本号
原子变量类
非阻塞算法通过原子变量类向外公开
Java中的原子类是用CAS思路构建的
Java中的原子类是用CAS思路构建的
分类
基本类型
AtomicBoolean
AtomicInteger
AtomicLong
引用类型
AtomicReference
引用类型原子类
AtomicMarkableReference
带有标记位的引用类型原子类
AtomicStampedReference
带有版本号的引用类型原子类
数组类型
AtomicIntegerArray
AtomicLongArray
AtomicReferenceArray
引用类型数组原子类
属性更新器类型
AtomicIntegerFieldUpdater
整型字段的原子更新器
AtomicLongFieldUpdater
长整型字段的原子更新器
AtomicReferenceFieldUpdater
引用类型字段的原子更新器
原子变量和CAS的应用
NumRange
伪数字生成器
锁与原子变量的性能比较
线程的计算量大时,资源的竞争不激烈
原子变量的性能优于锁
线程的计算量小时,资源的竞争激烈
锁的性能优于原子变量
产生以上结果的原因:原子变量使用CAS,在资源被占用时CAS会立即返回失败结果并重试
资源竞争激烈时,CAS重试次数会暴增
而实际情况是,常见的竞争程度上,原子变量性能优于锁(其中一个原因包括CAS具有良好的可伸缩性,而锁会降低程序的可伸缩性)
资源竞争激烈时,CAS重试次数会暴增
而实际情况是,常见的竞争程度上,原子变量性能优于锁(其中一个原因包括CAS具有良好的可伸缩性,而锁会降低程序的可伸缩性)
非阻塞算法
如果一个算法使得一个线程的失败或挂起不会导致其他线程一起失败或挂起,那么这种算法就被称为非阻塞算法
非阻塞算法中通常不会出现死锁和优先级反转,但是存在饥饿和活锁问题
许多数据结构都可以用到非阻塞算法,如:栈,队列,优先队列,散列表等
非阻塞的栈
非阻塞的链表
原子的域更新器
16. Java内存模型
什么是内存模型,为什么需要它
问题:一个线程执行num=3;结果其他线程读取到的值不是3
原因有很多
编译器生成的指令顺序和源代码中的顺序可能不同
编译器会把变量存在寄存器,而不是内存中
处理器采取乱序或并行的方式执行指令
缓存可能会改变将写入变量提交到主内存中的次序
保存在处理器本地缓存中的值对其他处理器中是不可见的
平台的内存模型
每个处理器都拥有自己的缓存,并且定期与主内存进行协调
在不同架构的处理器中提供了不同级别的缓存一致性,其中一部分只提供最小的保证,即不同存储器在任意时刻从同一存储位置上访问到不同的值
要想处理器能在任意时刻都了解其他处理器正在进行的工作(时刻保证缓存一致性)需要很大的开销
所以处理器会放宽缓存的一致性换取性能
所以处理器会放宽缓存的一致性换取性能
特殊指令:内存栅栏,简称栅栏。当不同处理器需要共享数据时。栅栏能实现额外的存储协调保证
串行一致性:单处理器下程序的不会出现多线程下的数据并发异常的问题。但是在多处理器下不能提供串行一致性
重排序
Java内存模型简介
Happens-Before
“借助”同步
一种高级同步技术
使用Happens-Before结合其他某个顺序规则(监视器锁规则或volatile规则)对某个未被锁保护起来的变量的访问操作进行排序
使用Happens-Before结合其他某个顺序规则(监视器锁规则或volatile规则)对某个未被锁保护起来的变量的访问操作进行排序
发布
不安全的发布
缺少Happens-Before时会存在,一个线程创建了以一个对象,另一个线程访问到的对象却是一个被部分构造的对象
安全的发布
安全初始化模式
静态代码块会被提供额外的线程安全
双重检查加锁(Double Check Lock — DCL)
DCL存在不安全发布的问题,线程可能会看到一个被部分构造的对象
这个问题可以用volatile解决(给变量添加volatile关键字)
这个问题可以用volatile解决(给变量添加volatile关键字)
初始化过程中的安全性
final
0. 名词解释补充
时钟频率
主频有时也叫时钟频率,单位是MHz,用来表示CPU的运算速度
吞吐率
吞吐率原指一个业务系统在单位时间内提供的产量(或服务量)
Servlet
Servlet(Server Applet)是Java Servlet的简称,称为小服务程序或服务连接器
ServletContext
所有的Servlet都共享的一个对象,用于存放不同Servlet所共享的数据
RMI
Remote Method Invocation 远程方法调用
RPC
Remote Procedure Call Protocol 远程过程调用协议
套接字
Socket(套接字)可以看成是两个网络应用程序进行通信时,各自通信连接中的端点
IO & NIO
阻塞式IO 和非阻塞式IO
回调
回调函数就是一个通过函数指针调用的函数。如果你把函数的指针(地址)作为参数传递给另一个函数,当这个指针被用来调用其所指向的函数时,我们就说这是回调函数
Java中调用回调函数多指用反射调用指定方法
Java中调用回调函数多指用反射调用指定方法
静态初始化函数
如果类有静态代码块或静态变量,在加载类时将调用静态初始化函数,执行内容为静态代码块和静态变量的赋值
锁粒度细化 & 锁粒度粗化
逸出分析
解释执行
直接执行
1. 并发 简介
驱使并发出现的原因
资源利用率
公平性
便利性
处理器的调度单位
处理器的基本调度单位是线程,也就是说一个处理器同一时间内只能处理一个线程,一个线程同一时间只能被一个处理器处理
建模的简单性
一个线程/程序只处理一种类型的任务,这将使程序编写变得简单且容易测试
Servlet 一个接口只处理一种任务
框架—框架负责解决请求管理,线程创建,负载均衡等细节
线程带来的风险
代码可读性变差
当多线程同时访问和修改相同的变量时,为保证变量的线程安全性,需要在串行编程模型中引入非串行因素
安全性
永远不发生糟糕的事情
对应线程不安全问题
活跃性
某件正确的事情最终会发生
活跃性问题指原本要发生的事由于某种问题而迟迟不发生 死锁就是活跃性问题的一种
2. 线程安全性
对象的状态
从非正式意义上来说,对象的状态指存储在状态变量(例如实例和静态域)中的数据
对象中需要确保线程安全的数据——共享的可变数据
共享变量
可以有多个线程同时访问的变量
可变变量
变量的值可以在生命周期内发生变化
由于封装机制的存在,对象封装的越好就越容易实现线程安全
无状态的对象
定义:不包含任何域(但可以在类方法里包含局部变量,因为这些局部变量不是共享变量),也不包含对其他类中域的引用的类的对象
无状态的对象没有共享变量,所以 无状态的对象一定是线程安全的
ps:多数Servlet都是无状态的,从而极大降低了实现Servlet线程安全的复杂性
Java中的同步方式
synchronized
显式锁(Explicit Lock)
原子变量
volatile类型的变量
线程安全性的定义
当多个线程访问某个类时,(在主调代码中不需要任何额外的同步或协同)这个类始终都能表现出正确的行为,那么就称这个类是线程安全的
原子性
原子性和原子性操作
原子性
所有操作bai,要么全部du完成,要么全部不完成,不zhi可能停滞在中间某个环节
原子性操作
一组操作,要么同时成功,要么同时失败
非原子性操作
例:i++
i++本质上是i = i+1,看似一步的操作实际上分两部进行,是复合操作
在并发下,如果i不是局部变量,那么这种操作存在线程不安全问题
i++本质上是i = i+1,看似一步的操作实际上分两部进行,是复合操作
在并发下,如果i不是局部变量,那么这种操作存在线程不安全问题
竞态条件
竞态条件一个不规范的定义:
并发编程中,由于不恰当的执行时序而出现不正确的结果。导致产生这种结果的条件就是竞态条件
(换句话说就是,获取到正确的结果要取决于运气)
并发编程中,由于不恰当的执行时序而出现不正确的结果。导致产生这种结果的条件就是竞态条件
(换句话说就是,获取到正确的结果要取决于运气)
例如:在没有做同步的条件下执行count++;
例如:懒加载的单例模式,如果没有实现同步,将可能导致对象被创建多次
原子操作类
原子操作是线程安全的。与原子操作相对的是复合操作,复合操作是线程不安全的
Java中的原子操作类的类名形如:AtomicXxx
这些原子变量类在java.util.concurrent.atomic包中
加锁机制(synchronized)
要保持状态的一致性,就需要在单个原子操作中更新所有的状态变量
即使所有的状态变量本身都是线程安全的,如果在类中同时使用这些变量却没有实现同步也会导致线程不安全
内置锁/监视器锁/同步监视器
重入
某个线程试图获得一个已经由他自己持有的锁,那么这个请求会成功
用锁来保护状态
只有共享的可变变量才需要通过锁来保护
3. 对象的共享
可见性
举例:
num的值为1,线程A将num的值改为2,线程B访问num后得到的值是1
原因:线程B访问到的数据是旧数据。线程B没有发现线程A对其进行了修改
(线程A对对象的修改对于线程B是不可见的,这和JMM有关)
num的值为1,线程A将num的值改为2,线程B访问num后得到的值是1
原因:线程B访问到的数据是旧数据。线程B没有发现线程A对其进行了修改
(线程A对对象的修改对于线程B是不可见的,这和JMM有关)
失效数据
举例:num的值为1,线程A将num改为2,线程B访问num得到的值是1
此时num的有效值为2,线程B访问到的数据就是失效数据(因为num已经被修改了,只是线程B没有发现)
此时num的有效值为2,线程B访问到的数据就是失效数据(因为num已经被修改了,只是线程B没有发现)
最低安全性
线程读取一个没有进行同步的变量时,可能会得到一个失效值,但这个值至少是之前另一个线程设置的
而不是一个随机的值
而不是一个随机的值
非原子的64位操作
问题:
非volatile类型的double和long变量是64位的,JVM对其读写时将其分解为两个32位的操作
多个线程对其进行读写时,可能会出现A线程刚修改了高32位的数据(改了一半),B线程就去读数据
非volatile类型的double和long变量是64位的,JVM对其读写时将其分解为两个32位的操作
多个线程对其进行读写时,可能会出现A线程刚修改了高32位的数据(改了一半),B线程就去读数据
解决方案:用volatile声明他们,或用锁保护起来
加锁与可见性
加锁的含义不仅局限于互斥行为,还包括内存可见性
volatile关键字
作用:
1. JVM不会对volatile变量的操作进行重排序
2. volatile变量不会被缓存到寄存器或者对其他处理器不可见的地方
(一个处理器同一时间只能处理一个线程,即volatile变量不会存到其他线程不可见的地方,即voltile变量对其他线程是可见的)
1. JVM不会对volatile变量的操作进行重排序
2. volatile变量不会被缓存到寄存器或者对其他处理器不可见的地方
(一个处理器同一时间只能处理一个线程,即volatile变量不会存到其他线程不可见的地方,即voltile变量对其他线程是可见的)
volatile常用于修饰作为判断条件的布尔类型的变量
发布与逸出
发布一个对象:是对象能够在当前作用域之外的代码中使用,即对象的引用暴露给其他类
(这里的对象的作用域,多指对象声明所在的作用域)
ps:发布一个对象时,对象的非私有域中引用的对象也会被发布
(这里的对象的作用域,多指对象声明所在的作用域)
ps:发布一个对象时,对象的非私有域中引用的对象也会被发布
逸出:某个不该发布的对象被发布(许多情况下我们并不希望对象及其内部状态被发布)
this引用逸出
构造器中有外部对象的引用时,意味着外部引用能接触到this,这就存在对象未被构造完就被其他对象访问的危险
有时为了防止对象逸出,方法需要返回一个成员变量时
会返回成员变量的一个深拷贝或副本 而不是成员变量的引用
会返回成员变量的一个深拷贝或副本 而不是成员变量的引用
线程封闭
定义
一个共享的可变数据只能在单线程中被访问。(因为只能被单个线程中访问,所以这个变量不需要使用同步机制)
线程封闭技术分类
Ad-hoc线程封闭
维护线程封闭的职责完全由程序实现来承担(非常脆弱)
栈封闭
举例:方法中创建并使用局部变量
对于基本数据类型的局部变量(值类型)
即使方法return了这个变量接收方也无法获得基本类型的引用确保了基本类型的局部变量始终封闭在线程内
即使方法return了这个变量接收方也无法获得基本类型的引用确保了基本类型的局部变量始终封闭在线程内
对于引用类型的局部变量
为做到线程封闭,不要随便将引用发布出去
为做到线程封闭,不要随便将引用发布出去
ThreadLocal类
并发下执行同一个方法,不同的线程从ThreadLocal中获取的变量是不同的
不变性
不可变对象:对象被创建后其状态就不能被修改
对象创建以后其状态就不能修改
对象的所有域都是final类型
对象是正确创建的(在对象的创建期间,this引用没有逸出)
不可变对象一定是线程安全的
final域
final除了能保证对象的值或引用不能被修改外,还能确保对象初始化过程的安全性
实例:使用volatile类型发布不可变对象是线程安全的
安全发布
不正确的发布:正确的对象被破坏
举例:
不同线程同一时间观察的同一个对象的状态是不同的
或一个线程观察对象的状态后对象的状态突然发生了变化,导致线程自以为获取到了期望的数据
不同线程同一时间观察的同一个对象的状态是不同的
或一个线程观察对象的状态后对象的状态突然发生了变化,导致线程自以为获取到了期望的数据
安全发布的常用模式
在静态初始化函数中初始化一个对象引用
例如:public static Holder holder = new Holder();
将对象的引用保存到volatile类型的域或者AtomicReference对象中
将对象的引用保存到某个正确构造对象的final类型域中
将对象的引用保存到一个有锁保护的域中
例如将引用保存到ConcurrentHashMap,CopyOnWriteArrayList等线程安全容器
事实不可变对象
事实不可变对象:指那些没有使用同步机制,但是被发布后不会被程序主动修改的对象
和不可变对象的区别在于
不可变对象是不能修改其状态而做到线程安全
事实不可变对象是没有地方主动修改其状态而做到线程安全
不可变对象是不能修改其状态而做到线程安全
事实不可变对象是没有地方主动修改其状态而做到线程安全
可变对象
安全发布可变对象,需要使用同步机制在对象的类的内部保护起来再发布出去
安全地共享对象的使用策略
线程封闭
线程封闭的对象只能由一个线程拥有,对象被封闭在该线程中,并且只能由这个线程修改
只读共享
在没有额外同步的情况下,共享的只读对象可以由多个线程并发访问,但任何线程都不能修改它。
共享的只读对象包括不可变对象和事实不可变对象
共享的只读对象包括不可变对象和事实不可变对象
线程安全共享
线程安全的对象在其内部实现同步,因此多线程可以通过对象的共有方法来规范而不需要进一步的同步
保护对象
被保护的对象只能通过持有特定的锁来进行访问。保护对象包括封装在其他线程安全对象中的对象,以及已发布的并且由某个特定锁保护的对象
4. 对象的组合
设计线程安全的类
收集同步需求
变量的不可变条件
用于判断变量的值是否有效的条件
例如,你希望int num的值永远>0
例如,你希望int num的值永远>0
变量的后验条件
用于判断变量执行操作后是否还满足不可变条件
例如:在num--后判断num的值是否<=0
例如:在num--后判断num的值是否<=0
类的不变性条件和后验条件约束了在对象上有哪些状态和状态转换是有效的
依赖状态的操作
某个操作中包含有基于状态的先验条件
例如:生产者,消费者模型。消费者需要等待商品数>0时才能消费
状态的所有权
一个对象包含很多域,持有对象的引用就能控制对象的域。所以要谨慎考虑对象的发布
实例封闭
将数据封装在对象内部,可以将数据的访问限制在对象的方法上,从而更容易确保线程在访问数据时总能持有正确的锁
举例:Collections.sychronizedList及类似方法,将线程不安全的类/对象封装到一个线程安全的类中。
Java监视器模式(同步监视器)
内置锁(this)
私有的锁对象(成员变量的锁)
线程安全性的委托
多个线程安全的类组合而成的类不一定是现成安全的。
能否将线程的安全性委托给这些线程安全的成员变量就是线程安全性的委托要解决的问题
能否将线程的安全性委托给这些线程安全的成员变量就是线程安全性的委托要解决的问题
无状态的类,不可变的类,只有一个线程安全的成员变量的类 都是线程安全的
独立的状态变量
类中有多个线程安全的变量,这些变量独立使用,互不依赖。那么类的线程安全性就可以直接委托给成员变量
委托失效
多数委托失效都是因为多个变量配合使用,且这些操作没有进行同步。尽管他们各自是线程安全的,但是整体上看并不是安全的
发布底层的状态变量
如果状态变量是线程安全的,且没有任何不变性条件或前后验条件约束它,那么就能安全地发布它
给现有的线程安全类中添加功能
客户端加锁机制
将目标类封装到自定义的类中(目标类对象设置为public),拓展方法使用的锁应该是目标类对象的锁,而不是自定义类的锁
组合
将目标类封装到自定义的类中(目标类对象设置为private),实现目标类实现的接口,在接口方法中调用目标类对象的方法。目标类对象不再对外暴露,而是通过自定义类操作目标类
5. 基础构建模块
同步容器类
同步容器类的问题
同步容器类是现成安全的,但是当多线程并发执行复合操作时容器会出错
例如:迭代,线程A在迭代遍历容器时线程B删除了一个元素。导致遍历时抛出ArrayIndexOutOfBoundsException
例如:迭代,线程A在迭代遍历容器时线程B删除了一个元素。导致遍历时抛出ArrayIndexOutOfBoundsException
迭代器与ConcurrentModificationException
容器出现并发异常时会抛ConcurrentModificationException异常
隐藏迭代器
并发容器类
ConcurrentHashMap
使用分段锁,保证了一定数量的线程能并发修改Map,任意数量的线程能并发访问Map
此容器还提供了许多复合操作的方法,“没有则添加”,“相等则替换”等方法
CopyOnWriteArrayList
写时复制容器。缺点是每次修改容器时都会复制底层数组,这需要一定的开销
...
阻塞队列
BlockingQueue
生产者-消费者设计
串行线程封闭
生产者将对象的引用较付给消费者。如果消费者不主动发布对象,那么这个对象就实现了线程封闭
双端队列与工作秘取
Deque和BlockingDeque
双端队列可用于消费者之间
每个消费者持有一个双端队列,当一个消费者提前完成队列中的任务时,可以从其他消费者的双端队列的末尾“偷偷”获取任务
每个消费者持有一个双端队列,当一个消费者提前完成队列中的任务时,可以从其他消费者的双端队列的末尾“偷偷”获取任务
阻塞方法和中断方法
同步工具类
CountDownLatch——闭锁
FutureTask
Semaphore——信号量
CyclicBarrier——栅栏
实战:构建高效且可伸缩的结果缓存
6. 任务执行
在线程中执行任务
串行地执行任务
一个线程逐个处理任务
显式地为任务创建线程
主线程将任务分配给创建的新线程
无限制创建线程的不足
内存会炸
Executor框架
线程池
执行策略
哪个线程执行任务
最多有多少个线程并发执行
阻塞队列中最多有多少个任务
任务的拒绝策略
执行任务前后应该执行哪些动作
...
Executor的生命周期
ExecutorService实现了Executor接口,添加了一些管理线程池声明周期的方法
ExecutorService的生命周期
运行
关闭
已终止
Executor执行的任务有四个生命周期阶段
创建
提交
开始
完成
延迟任务与周期任务
Timer
TimerTask
ScheduledThreadPoolExecutor(ThreadPoolExecutor的子类)
找出可利用的并行性
示例:串行的页面渲染器
携带结果的任务Callable和Future
示例:使用Future实现页面渲染器
Future执行的任务可手动调用cancel方法取消任务,也可以使用其他方法获取到当前任务的执行状态
在异构任务并行化中存在的局限
ExecutorCompletionService(CompletionService接口的实现列 ):一个封装了Executor和BlockingQueue的类
其作用相当于一组任务的句柄
其作用相当于一组任务的句柄
示例:使用CompletionService实现页面渲染器
为任务设置时限
示例:旅游预订门户网站
Future的get方法可以设置任务的执行时间,如果超时就抛TimeoutException
7. 取消与关闭
线程中断是一种线程间的写作机制,能够使一个线程终止另一个线程当前的工作
我们很少希望某个任务,线程活服务立即停止,这样会导致共享数据可能处于不一致的状态
我们希望,当需要停止时,它们首先会清除当前正在执行的工作,然后再结束。而这些行为应该又任务本身去做,因为他们更清楚如何执行清除工作
我们希望,当需要停止时,它们首先会清除当前正在执行的工作,然后再结束。而这些行为应该又任务本身去做,因为他们更清楚如何执行清除工作
任务取消
中断
每个线程都有一个boolean类型的中断状态,调用中断方法时此状态变为true
Thread的中断方法
public void interrupt() // 中断,将中断状态设为true
public boolean isInterrupted() // 返回目标线程当前的中断状态
public static boolean interrupted() // 清除中断状态并返回清除前的状态值
阻塞库方法
Thread.sleep和Object.wait等方法,在检查到线程是中断状态时会
1. 清除中断状态
2. 抛出InterruptedExcepiton异常
1. 清除中断状态
2. 抛出InterruptedExcepiton异常
响应中断异常
在线程处于中断状态时调用阻塞库方法抛出Interrupted异常
如何处理异常?
如何处理异常?
传递异常。把异常向外抛,让调用者处理异常
自行处理,例如恢复中断状态等
通过Future实现取消
任务取消后,任务不再执行
Future接口提供cancel方法用于取消任务
处理不可中断的阻塞
一些阻塞方法会不断检测当前线程是否被中断
如果被中断,此阻塞方法会响应中断 (向外抛Iterrupted异常或自行处理)
如果被中断,此阻塞方法会响应中断 (向外抛Iterrupted异常或自行处理)
还有写阻塞方法不检查当前线程师是否被中断
对于这些方法,线程的中断状态没有意义
对于这些方法,线程的中断状态没有意义
使用一些方法,让这些阻塞方法在线程中断时做出响应
IO的read和write阻塞时不会对中断做出响应
间接中断方式:关闭底层的套接字,让IO对象抛SocketExption终止阻塞
间接中断方式:关闭底层的套接字,让IO对象抛SocketExption终止阻塞
获取某个内置锁时阻塞,无法响应中断
解决方式:内置锁无法响应中断,显式锁Lock类提供了lockInterruptibly,这个方法允许等待锁时能响应中断。见第13章
解决方式:内置锁无法响应中断,显式锁Lock类提供了lockInterruptibly,这个方法允许等待锁时能响应中断。见第13章
用newTaskFor封装非标准的取消
AbstractExecutorService的newTaskFor方法用于将Callable或Runnable对象封装到RunnableFuture中
你可以创建一个Callable或Runnable的实现类,创建newTaskFor方法
再创建一个ThreadPoolExecutor的子类,重写newTaskFor方法。进行一个判断,如果任务是你自定义的Callable或Runnable类,就调用自己的newTaskFor,如果不是就调用AbstractExecutorService的newTaskFor
在调用自己的newTaskFor方法时将做这些事
停止基于线程的服务
应用程序拥有生命周期方法,持有服务。服务持有工作者线程(应用程序不能持有工作者线程)
示例:日志服务
日志服务基于多生产者,单消费者模型
如何安全地关闭日志服务?
关闭ExecutorService
shutDownNow
shutDown
awaitTermination
”毒丸“对象
另一种关闭生产者消费者服务的方式
是基于FIFO的工作队列的服务中使用的关闭方式
是基于FIFO的工作队列的服务中使用的关闭方式
1. 当需要关闭服务时生产者向队列中put特殊的元素(毒丸)
2. 消费者从队列中get元素时,如果获取到的元素时毒丸就设法退出服务(break、return等)
ps:缺点,生产者需要知道消费者的数量,因为生产者需要为每个消费者都提供一个毒丸对象
shutdownNow的局限性
shutdownNow方法会返回那些还未执行的任务
但是,我们无法知道哪些任务是关闭ExecutorService之前开始执行且关闭后还未执行完的
(这需要我们自己解决)
但是,我们无法知道哪些任务是关闭ExecutorService之前开始执行且关闭后还未执行完的
(这需要我们自己解决)
处理非正常的线程终止
线程的非正常终止主要时由抛出的异常没有被处理而导致的
处理方式
在创建,并启动线程的逻辑中用try-catch-finally,在finally块中执行一些操作,例如重新启动一个线程执行此任务、清理线程创建的临时文件等
为线程池设置一个UncaughtExceptionHandler对象,用于处理未被捕捉的异常
通过为ThreadPoolExecutor设置ThreadFactory来设置UncaughtExceptionHandler
JVM关闭
关闭钩子
在正常关闭JVM时会调用所有关闭钩子
关闭钩子是通过Runtime.addShutdownHook注册的但还未执行的线程
可以使用关闭钩子执行一些清理工作,例如清理临时文件或停止一些服务
可以使用关闭钩子执行一些清理工作,例如清理临时文件或停止一些服务
守护线程
新线程将继承创建它的线程的守护装状态
普通线程创建的线程默认是普通线程
守护线程创建的线程默认是守护线程
普通线程创建的线程默认是普通线程
守护线程创建的线程默认是守护线程
终结器
JVM关闭时,会执行含有finalize方法的对象的finalize方法。不推荐使用
终结器在关闭钩子执行后执行
8. 线程池的使用
在任务与执行策略之间的隐形耦合
线程饥饿死锁
有些任务时需要依赖其他任务的
例如:A任务需要B任务的执行结果,但是B任务因为没有空余线程而被保存在阻塞队列中。结果A任务和B任务都无法执行。这是饥饿死锁的表现之一
例如:A任务需要B任务的执行结果,但是B任务因为没有空余线程而被保存在阻塞队列中。结果A任务和B任务都无法执行。这是饥饿死锁的表现之一
运行时间较长的任务
多数可阻塞方法都能设置超时时间,可以通过设置超时时间解决这个问题
配置ThreadPoolExecutor
线程的创建与销毁
管理队列任务
各种阻塞队列
饱和策略/拒绝策略
在没有空闲线程执行任务,且阻塞队列页也没有位置保存任务时执行哪种操作
这取决于饱和策略 / 拒绝策略
这取决于饱和策略 / 拒绝策略
线程工厂
public interface ThreadFactory {
Thread newThread(Runnable r);
}
Thread newThread(Runnable r);
}
ThreadFactory的实现类可以在创建线程时执行各种操作,例如给线程指定一个UncaughtExceptionHandler
调用构造函数定制ThreadPoolExecutor
扩展ThreadPoolExecutor
ThreadPoolExecutor有些方法等待子类实现
beforeExecute // 在任务执行前执行,如果方法抛异常,将不再执行任务
afterExecute // 在执行任务后执行
terminated // 在关闭线程池时执行,可用于释放线程池的生命周期中分配的各种资源
递归算法的并行化
0 条评论
下一页