目录
活跃性危险
死锁(deadlyembrace)
jvm对死锁的处理:当一组java线程发生死锁,这些线程永远不会再使用了.根据线程完成的工作的不同,可能造成应用完全停止,或者某个特定的子系统完全停止,或者性能降低,恢复的唯一方式是重启.并且希望不会再发生死锁.
问题: 线程池中发生死锁有何影响?
锁顺序死锁
1 | public class LeftRightLock{ |
发生死锁的原因: 两个线程试图以不同的顺序获得锁.如果按照相同的顺序获取锁,则不会出现循环加锁的依赖性.
动态的锁顺序死锁
1 | public void transferMony(Account from,Account to,DollarAmount amount){ |
发生死锁的原因:存在嵌套的锁获取操作,无法控制锁的顺序.
可通过制定锁的顺序来避免死锁.通过System.identityHashCoe
方法指定顺序.
协作对象之间发生死锁
有些获取多个锁的操作,在不同的对象不同方法中,其未必在同一个方法中被获取.
活跃性问题
在持有锁时调用外部方法,将出现活跃性问题.在这个外部方法中,可能获取其它锁(可能会产生死锁),或者阻塞 ,将导致其他线程无法及时获得当前线程持有的锁.
资源死锁
资源池通常采用信号量实现,若一个任务需要连接两个数据库,且请求两个资源不会始终遵守同一顺序,则可能A持有D1等待D2,B持有D2等待D1.
线程饥饿死锁
如果某些任务需要等待其他任务的结果,这些任务往往是产生死锁的主要来源,有界线程池/资源池与相互依赖的任务不能一起使用。
死锁的避免与诊断
如果一个程序必须使用多个锁,那么在设计时必须考虑锁的顺序:尽量减少潜在的加锁交互数量,将获取锁时需要遵循的协议写入文档并始终遵守。
使用细粒度锁的程序中,通过两阶段策略(Two-Part Strategy)连检查代码中的死锁:
找出在什么地方获取多个锁(使这个集合尽量小),对这些实例进行全局分析,从而确保它们在整个程序中获取锁的顺序保持一致.
尽可能使用开放调用,能极大的简化分析过程.如果所有的调用都是开放调用.那么要发现获取多个锁的实例时非常简单,通过代码审查,或者借助自动化的源码分析工具.
支持定时锁
使用Lock类中定时tryLock功能来代替内置锁机制.当使用内置锁时,只要没有获得锁,就会永远等待下去.而显示锁则可以指定超时时间,超过该时间后,tryLock返回false.
通过线程转存储信息来分析死锁
Jvm 可通过线程转存储(Thread Dump)分析死锁.
jstack -pid
其他活跃性问题
死锁是常见活跃性危险,还有其他活跃性危险:饥饿,丢失信号,活锁.
饥饿
线程由于无法访问它所需要的资源而不能继续执行,就发生了饥饿.
引发饥饿最常见的资源是cpu时钟周期.
如果java应用中,对线程的优先级设置不当,或者持有锁时,执行一些无法结束的结构(while(true),或者无限的等待资源),那么也可能导致饥饿.
避免设置线程优先级,这会增加平台依赖性.
活锁
活锁不会阻塞线程,但程序也不会继续执行,线程总会不断尝试,不断失败.
- 活锁通常发生在处理事务消息的应用中:如果不能成功处理消息,那么消息处理机制将回滚整个事务,并将其重新放到队列的开头.这种形式的活锁通常是由过度的错误恢复代码造成的,其错误地将不可修复的错误作为可修复的错误.
- 多个相互协作的线程都对彼此进行响应从而修改各自的状态,并使得任何一个线程都无法继续执行时,发生活锁.(过于礼貌的人在路上互相让路),解决方法:加入随即等待时间.
性能与可伸缩性
性能
性能提升意味着用更少的资源做更多的事情.
尽可能让cpu都处于忙碌状态(有效计算)
伸缩性
可伸缩性: 增加计算资源(cpu,内存,存储容量或者I/O带宽),程序的吞吐量或者处理能力相应地增加.
应用程序的性能指标:服务时间,延迟时间,吞吐率,效率,可伸缩性以及容量.服务时间,延迟时间用于描述程序的”运行速度”,吞吐量及生产量,描述程序”处理能力”
- 对服务器应用程序来说,可伸缩性,吞吐量和生产量,比”多块”更重要.
性能决策的权衡
避免不成熟的优化,首先使程序正确.然后再提高运行速度-如果需要提高
性能决策中包含多个变量,且依赖运行环境,再决策之前:
- “更快”的含义是什么?
- 该方法在什么条件运行的更快?低负载还是高负载,大数据集还是小数据集?能否通过测试结果来验证你的答案?
- 这些条件在运行环境中的发生频率?能否通过测试结果验证?
- 在其他不同条件能否使用/
- 实现这种性能提升需要付出那些隐含代价?权衡是否合适.
- 对性能的调优时,一定要有明确的性能需求
- 对性能调优后,需要一个测试程序来模拟真实的配置及负载(以测试为基准, 不要猜测).
amdanhl定律
speedDup <= 1/(f+(1-f)/n)
所有并发程序中都包含一些串行部分。
线程引入的开销
上下文切换
应用程序,jvm,操作系统共用同一组cpu,jvm和操作系统代码消耗越多cpu时钟周期,应用程序可用的cpu时钟周期就越少。
上下文切换的开销并不只是jvm和操作系统的开销,当一个新的线程切换进来时,其所需要的数据可能不在当前cpu的缓存中,因此会有些缓存缺失,第一次运行速度更慢。
调度器为每个可运行线程设置最小执行时间(即使有许多其他的线程正在执行),它将上下文切换的开销分摊到更多不会中断的执行时间上,从而提高整体吞吐量。
当线程由于等待某个发生竞争的锁而被阻塞时,jvm会将这个线程挂起,允许它被交换出去。
上下文切换的消耗随着不同平台变化而变化,在多数通用的处理器中,上下文消耗相当于 5000~10000个时钟周期,也就是几微秒。
- unix的vmstat命令和windows的perfmon工具都能报告上下文切换次数以及在内核中执行时间所占比例信息,如果内核占用率较高(>10%),那么通常表示调度活动发生的比较频繁。很可能时io或竞争锁导致的。
内存同步
同步操作的性能开销包含多个方面。
有竞争的同步与无竞争的同步:
- 无竞争的同步开销仍然不为0(20~250个时钟周期),不过影响也微乎其微.jvm还会对其进行优化.
阻塞
不同的线程在竞争锁时,失败的一方会发生阻塞,jvm在实现阻塞行为时,可以采用自旋,或者通过操作系统将线程挂起这两种方式.
- 自旋:适用等待时间较短
- 挂起:适用等待时间长
大多数jvm还是选择将线程挂起.
当线程(阻塞或者io阻塞)需要被被挂起,在这个过程,会包含两次额外的上下文切换.
减少锁的竞争
串行操作会降低可伸缩性,上下文切换也会降低性能.在锁上发生竞争会同时导致这两个问题.减少锁的竞争能够提高性能和可伸缩性.
- 并发程序中,对可伸缩性最大的威胁就是对资独占方式的资源锁
影响发生锁竞争的可能性:锁的请求频率 & 锁持有时间
- little定律(排队理论):在一个稳定的系统中,顾客的平均数量=平均到达率*平均停留时间
降低锁的竞争程度:
- 减少锁的持有时间
- 降低锁的请求频率
- 使用有协调机制的独占锁,这些机制允许更高的并发性.
缩小锁的范围-减少锁的持有时间
锁持有时间过长,会限制可伸缩性.如:某个操作会持有锁2ms,并且所有操作都需要这个锁,那么无论用有多少个空余处理器,吞吐量也不会超过500个操作/s.
jvm会对锁进行优化,同步代码块虽然尽量小,但也不能过小,一些需要原子操作的必须包含在同一代码块中,同步也需要一定的开销,当把一个同步代码块分解成多个同步代码块,jvm在对锁粒度粗化操作,会将分解的代码块重新合并,甚至会导致性能降低.
减少锁的粒度-降低锁请求频率
降低锁请求频率: 可通过锁分解或者锁分段技术实现.这些技术能减小锁的粒度,并且提高可伸缩性,但是死锁风险也越高.
热点域
当每个操作都会请求多个变量时,锁的粒度很难降低(这是在性能与可伸缩性之间相互制衡的一方面).常见的优化方式是将一些反复计算的结果缓存起来,会引入一些热点域(hot filed)
,这些hod-field往往会影响可伸缩性.
代替独占锁的方法
放弃使用独占锁,用并发容器,读写锁,不可变对象以及原子遍量.
监测cpu利用率
cpu,io等监控参考 linux uptime 平均负载
判断增加cpu是否可提升性能时,可根据 vmstat
命令进行判断,有一栏信息是当前处于可运行但没有运行的线程数量.
锁竞争:随机打印一下线程转存储信息,竞争激烈的总会出现 waiting to lock monitor...
减少上下文切换
并发程序的测试
- 安全性:不发生任何错误行为
- 活跃性:某个良好的行为终将发生
- 吞吐量:一组并发任务中已完成任务所占比例
- 响应性:请求从发出到完成之间的时间
- 可伸缩性:增加更多资源的情况下(cpu)吞吐量的提升情况