死锁(获得需要的资源前不放弃已获得的资源,造成阻塞,此资源可以是锁) 1.数据库系统可检测死锁以及从死锁恢复:当它监测到一组事物发生死锁(通过表示等待关系的有向图中搜索),将选择一个牺牲者并放弃这个事物(牺牲者释放所持有资源),程序可重新执行被强行中止的事物(竞争资源的食物都已完成时) 2.JVM解决死锁问题:A.当一组Java线程发生死锁,这些线程永远不能再使用 B.可能造成: 应用程序停止,子系统停止,性能降低 C.恢复应用唯一办法:中止并重启它
死锁类型 1.锁顺序死锁:A.原因:两个线程试图以不同顺序来获得相同的锁 B.解决:若所有线程以固定顺序获得锁,则不会发生 C.解决:验证锁一致性,对程序加锁行为进行全局分析 2.动态的锁顺序死锁原因:无法确定在锁顺序上有足够控制权避免死锁(控制权:A.获得目标对象锁 B.通过原子方式更新目标对象 C.同时不破坏不变性条件,例如账户不为负数) 3.动态顺序死锁解决:A.检查是否存在嵌套的锁获取操作 B.定义锁顺序:整个程序须按照此顺序获取锁 C.实现:使用System.identityHashCode制定,该方法返回Object.hashCode返回的值,利用键值给对象排序 D.TieBreaking加时赛锁(若两个对象返回相同键值) 4.在协作对象间发生死锁A.本质也是两个线程按照不同顺序获得锁(此锁不一定在同一个方法),造成死锁 B.防范:若在持有锁时调用外部方法,就需防范死锁(外部方法可能获取其他锁,或阻塞时间过长,导致其他线程无法及时获得当前被持有的锁) 5.方法调用:抽象屏障,无需了解被调用方法中所执行操作,持有锁时调用外部方法难以分析 6.开放调用:A.定义:调用方法时不需持有锁 B.类似采用封装机制提供线程安全的方法 C.优点:易死锁分析,活跃性更简单,易编写,易于找出需要多个锁的代码路径,提高可伸缩性 7.资源死锁:多个线程相互持有彼此资源又不释放自己持有的资源时,会发生死锁
死锁的避免与诊断 1.方法:A.考虑锁的顺序,减少潜在加锁交互数量 B.找出使用多个锁地方并进行全局分析 C.多使用开放调用(BC为细粒度锁的两阶段策略) 2.使用定时锁(显式使用Lock类中定时tryLock代替内置锁机制):A.作用:检测死锁和从死锁中恢复 B.过程:使用内置锁,若没获得锁将一直等待。显式锁可指定超时时限,获取锁等待超过该时限后tryLock会返回一个失败信息(设置很长时限,可防止发生意外情况) 3.定时锁失败:无需知道失败原因,能记录所发生的失败和此次操作其他信息,并平缓的方式重新启动计算,而非关闭整个进程 4.定时锁优点:即使系统没有始终使用定时锁,定时锁获取多个锁可有效应对死锁。若获取锁超时,可使放锁,后退并在一段时间后再次尝试,消除死锁恢复程序(只适用于同时获取两个锁,不适用于嵌套方法请求多个锁,外层锁无法释放)
通过线程转储信息分析死锁(JVM通过线程转储识别死锁) 1.生成线程转储前:JVM在等待关系图找死锁,找出则获取其相应信息(涉及锁,线程和位置) 2.线程转储内容:A.各个运行中线程的栈追踪信息(类似于发生异常时的栈追踪信息) B.加锁信息(每个线程持有哪些锁,哪些栈帧可获取这些锁,被阻塞线程正等待哪个锁) 3.使用显式Lock类而非内部锁:A.结果:Lock锁获得的信息比内置锁获得的信息精度低 B.原因:内置锁与获得它们所在的线程栈帧相关联,显式Lock只于获得它的线程相关联 4.诊断死锁(JVM工作):哪个锁导致死锁,涉及哪些线程,它们持有哪些锁,带来的间接影响 其他活跃性危险(饥饿,丢失信号,活锁等) 1.饥饿(线程无法访问它所需要的资源)原因:程序对线程优先级使用不当,或者持有锁时执行一些无法结束的结构(引发饥饿常见资源:CPU时钟) 2.线程优先级:Thread API中有10级(只作为参考),JVM将它们映射到操作系统优先调度级后,可能会被映射到同一优先级(操作系统优先级数量少于10) 3.避免饥饿方法:避免使用线程优先级,这会增加平台依赖性。或使用默认线程优先级 4.糟糕的响应性:A.例如:CPU密集型后台任务,会与事件线程共同竞争CPU B.解决方法:发挥线程优先级作用,降低后台优先级,提高前台响应性 5.活锁(不会阻塞线程,也不能继续执行,不断重复执行相同操作,且总失败)发生情况:A.处理事务消息,每次都传递到错误处理器,事务回滚到队列开头,处理器被反复调用 B.多个相互协作的线程都对彼此进行响应从而修改状态 6活锁解决办法:在重试机制中引入随机性(等待随机长度的时间和回退可有效避免活锁发生)
对性能的思考(用更少的资源做更多的事) 1.资源密集型操作:操作性能由于某种特定资源而受限制(CPU密集型,数据库密集型等) 2.多线程额外开销(开销可能大于单线程):A线程间协调(加锁,触发信号,内存同步等) B.增加上下文切换 C.线程的创建与销毁 D.线程的调度等 3获取更好性能方法:更有效利用现有处理资源(CPU尽可能忙碌),出现新的处理资源时使程序竟可能利用这些新资源(忙碌的前提下,增加处理器)
性能与可伸缩性 1.程序性能衡量:尽量采用多个指标,衡量程序运行速度(服务时间,等待时间),衡量处理能力(生产量,吞吐量) 2.可伸缩性含义:增加计算机资源(CPU,内存,存储容量,I/O带宽)时,程序处理能力提升情况 3.调优目的:A.传统性能调优:用更小代价完成工作 B.可伸缩性调优:将问题的计算并行化 4.单线程性能和可伸缩性关系:大多数提高单线程程序性能技术,都会破坏可伸缩性(融合三层程序模型到单个应用,即使增加计算机资源,处理能力也不会提高) 5.单线程和可伸缩性比较:A.单线程应用避免许多开销(任务排队,线程协调,数据复制等) B.可伸缩性:单线程处理能力达到极限,接受每个工作单元消耗更多计算资源换取处理更高负载 6.增加某种形式成本来降低另一种形式的开销(需评估各种性能权衡因素) A.通过安全性换取性能 B.通过开销换取安全性 C.牺牲可读性或可维护性换取性能优化等等
Amdahl定律(使并行工作和串行工作合作发挥最高效率,取决于双方比重) 1.Amdahl定律描述:A.最高加速比speedup<1/(F+(1-F)/N) B.F指必须被串行执行部分,N为处理器个数(N趋紧无穷时,speedup趋近1/F) 2.吞吐量: 网络、设备、端口其他设施单位时间内成功地传送数据的数量(比特,字节为单位) 3.处理器利用率:A.公式:处理器使用率=最高加速比/处理器个数 B.特点:随着处理器增加,即使串行占比很小,也会极大地限制计算机资源增加时提升的吞吐量 4.串行部分隐藏的位置(并发程序一般都有):A并行工作线程共享工作队列,若加锁保护队列,访问队列则为串行 B.对结果处理,计算结果写入日志或保存到某个数据(多个线程共享) C.应用程序架构中,可比较增加线程时吞吐量变化,并观察可伸缩性变化推断串行部分差异 5.Amdahl定律的应用:估算执行过程中串行所占比例,面对可能出现的可伸缩性局限有一定认识(例如对锁分解或锁分段的分析)
线程引入的开销(多线程的性能提升必须超过并发导致开销) 1.上下文切换含义:可运行线程数大于CPU数,操作系统将某个正在运行的线程调度出(保存其执行上下文),使其他线程可以使用CPU(其执行上下文设置为当前上下文) 2.上下文切换造成的开销:A.JVM,操作系统,应用程序使用一组CPU,线程调度需访问操作系统和JVM共享的数据结构,访问的越多,应用程序CPU时钟就越少 B.新线程切换进来,可能需要的数据不在当前处理器本地缓存,缓存缺失使首次调度更慢 C.线程因竞争锁而被阻塞,无法使用完整调度时间片,且更多的上下文切换,降低吞吐率,增加开销 3.内存同步开销:Synchronized和volatile提供可见性使用内存栅栏(刷新缓存,使缓存无效,刷新硬件的写缓冲,停止执行管道),内存栅栏会抑制编译器优化操作,且大多操作不能重排序 4.非竞争内存同步开销解决:A.区分有竞争力同步和无竞争力同步(Synchronized会对其进行优化) B.JVM优化去掉一些不发生竞争的锁 C.更完备的JVM能通过逸出分析找出不会发布到堆的本地对象引用(原本封闭在栈的变量都自动变为线程本地变量) D.编译器可执行锁粒度粗化操作,将邻近同步代码块用同一个锁合并 5.竞争的同步开销(操作系统的介入):锁竞争失败线程阻塞A.自旋等待即循环不断尝试获得锁(适合等待时间短) B.操作系统挂起阻塞线程(适合等待时间长),但包含两次额外上下文切换,以及所有必要的操作系统操作和缓存操作
减少锁竞争(可伸缩性主要威胁就是独占方式的资源锁) 1.降低锁竞争程度方法:A.减少锁持有时间 B.降低锁请求频率 C.使用带协调机制的独占锁 2.缩小锁的范围,快进快出(减少锁持有时间)方法:A.与锁无关的代码移出同步代码块,尤其开销较大操作,可能被阻塞的操作 B.线程安全性委托给线程安全类,提升性能 3.缩小锁范围注意:A.同步代码块不能过小,原子方式执行的操作必须在同一个同步代码块 B.同步代码块分解为多个同步代码块也会增加开销 4.降低锁的粒度(降低线程请求锁频率): A.定义:若竞争一个全局锁,可将这些锁分布到更多锁上,降低竞争,阻塞线程将更少。B.方法:锁分段或锁分解 5.锁分解情况及方法:若锁保护多个相互独立变量,可分解为多个锁,每个锁保护一个变量 6.锁分段:A.方法:将锁分解技术进一步扩展为对一组独立对象上锁的分解 B.劣势:获取多个锁实现独占访问更加困难且开销更高(实例:concurrentHashMap中size对每个分段的计数) 7.避免热点域:若用锁分段,应满足分段前锁上竞争频率高于分段后锁保护数据竞争频率 8.替代独占锁的方法:A.并发容器 B.读-写锁(ReadWriteLock:多个读取不修改共享资源,单个写入操作必须以独占方式) C.不可变对象 D.原子变量(静态计数器,序列发生器等) 9.CPU若没有充分利用的原因(CPU利用率):A.负载不充足 B.I/O密集(通过iostat或perfom判断,或检测网络通信流量判断是否提高带宽) C.外部限制(需要等待外部服务) D.锁竞争(线程转储,查找发生竞争的线程) E.若CPU利用率一直很高,可适当增加处理器个数 10.尽量不用对象池:对象分配操作的开销比同步开销更低
比较Map性能 1.多线程下Map吞吐率比较:ConcurrentHashMap > ConcurrentSkipListMap > synchronized HashMap == synchronized TreeMap,前两种Map线程安全 2.同步封装容器(后两个Map)可伸缩性阻碍:A.Map中只有一个锁,只有一个线程可以访问 B.增加线程负载由非竞争变为竞争,将消耗时间用于上下文切换和调度延迟,性能反而变差 3. :ConcurrentHashMap特点:A.读操作不加锁(多线程并发访问不阻塞),写操作使用锁分段技术 B.线程数量增加,可表现更好的伸缩性,直到消耗和收益平衡
如何提高吞吐量 1.减少上下文切换(任务在运行和阻塞这两个状态之间转换) 2.请求服务不应太长:A.服务时间会影响服务质量 B.服务时间越长,存在越多锁竞争 3.如何缩短处理请求平均服务时间(取决于工作量):A.由发出请求线程完成(可能发生锁竞争被阻塞,造成更多上下文切换,反而增加服务时间) B.将I/O操作从请求处理线程中分离,转移到其他专门线程(消除输出流的竞争,锁的管理更简单)
并发测试分类(安全性测试和活跃性测试) 1.A.安全性定义:不发生任何错的行为 B.活跃性定义:某个良好的行为终究会发生 2.安全性测试:采用测试不变性条件,即某个类的行为是否和其规范保持一致 3.活跃性测试:A.进展测试和无进展测试(方法是否阻塞,算法是否死锁) B.性能测试:吞吐量(并发任务已完成所占比例),响应性(发出到完成的时间,也称延迟),可伸缩性
正确性测试(包含安全性测试) 1.并发类测试目的:找出需要检查的不变性条件和后验条件,类的规范中将给出其中大部分的条件,剩下编写测试时刻不断发现新规范 2并发类基本单元测试:类似串行上下文中执行的测试,可分析数据竞争前与并发性无关问题 3.并发基本属性测试:A.引入多线程,需将辅助线程的成功或失败信息传递回主测试线程(框架不能很好支持并发性测试) 4.java.util.concurrent一致性测试:需将故障与特定测试联系起来,,某些方法可在tearDown期间传递和报告失败信息,但每个测试必须等它所创建的全部线程结束后才能完成 5.测试方法的阻塞:A.测试这种行为,只有线程不再继续执行,测试才成功 B.方法阻塞后,还得解除阻塞:单独的线程中启动一个阻塞操作,等到线程阻塞再中断它,宣告阻塞成功,要求阻塞方法通过提前返回或抛出InterruptedException响应中断 6.用.Thread.getState()验证阻塞:不可靠A.被阻塞线程不进入WAITING或TIMED_WAITING状态,JVM可自旋等待实现阻塞 B.Object.wait或Condition.await等存在伪唤醒,即使线程等待未成真,也可能临时性转换到RUNNABLE C.目标线程进入阻塞状态也会消耗一定时间
安全性测试. 7.作用:发现并发中数据竞争引发的错误(尽量找容易检查属性),测试程序自身也是并发程序 8.测试生产者-消费者使用类的方法实现:A.元素插入队列时同时插入一个影子列表,删除时也从影子列表删除,结束后判断影子列表是否为空(缺点:影子列表需同步,会阻塞,干扰测试线程调度) B.对顺序敏感的校验和计算函数来计算所有入列元素和出列元素校验和 C.多个生产者-消费者:对元素出入列顺序不敏感的校验和函数,运行完可将多个校验和以不同顺序组合起来(否则需同步,构成并非瓶颈)。 9.校验和函数特点:A.不能让编译器提前猜到校验和值 B.随机方式生成的测试数据(不合适的随机数生成器:线程安全,额外同步开销,执行时序产生耦合关系) C.简单伪随机函数即可 10.使用两个CountDownLatch或CyclicBarrier(解决线程并未并非执行):初始化将计数值指定为工作者线程数+1,开始和结束时,工作者线程和测试线程都在栅栏处等待 11.常见错误:A.执行插入和取出代码忘记实现互斥行为(可用synchronized或ReentrantLock) B.测试要求执行完一定数量操作才能停止,若抛出异常测试永不结束:测试框架应放弃未在规定时间内完成的测试
资源管理测试 1.资源泄漏:妨碍垃圾回收器回收内存,导致资源耗尽及应用程序失败 2.测试对内存不合理占用:堆检查工具抓去堆快照 3.回调函数:执行在对象生命周期一些已知位置,适合判断不变形条件是否破坏(例如:线程池需要时创建新线程,回收空闲线程,线程池能否按照预期方式扩展等) 4.产生更多交替操作(作用:测试并发错误需反复执行多次提高发现错误概率)方法:A.访问共享状态的操作中,使用Thread.yield产生更多上下文切换 B.使用时间较短的sleep,虽然更新慢,但可靠
性能测试(衡量典型测试用例中端到端性能) 1.目的:A.测试生产者向消费者提供数据时吞吐量 B.调整各种不同限制(线程数量、缓存容量等可能依赖于具体平台特性,需动态分配) 2.过多的线程:若无足够计算量,大量时间消耗在线程阻塞和解除阻塞等操作使性能下降 3.多种阻塞队列算法·比较:A.BoundedBuffer运行效率不高原因:put和take方法含有可能发生竞争的操作B.LinkedBlockingQueue:有更好的内存分配与GC开销,链表队列的put和take方法支持并发性更高的访问(优化后链接队列算法能将头节点与尾节点更新操作分离) C.ArrayBlockingQueue:基于数组的队列,可伸缩性低于LinkedBlockingQueue D.如何提高算法可伸缩性:算法能多执行一些内存分配(通常本地)操作降低竞争程度 4.响应性平衡(经过多长时间才能执行完成):A.非公平信号量(隐蔽栅栏) :完成时间短吞吐性高,但变动性大B.公平信号量(开放栅栏):同步控制实现更高公平性,降低变动性但会降低吞吐量,开销主要为线程阻塞 5.非公平模式吞吐:A.缓存过小,将导致非常多上下文切换次数,即使非公平模式吞吐量也很低 B.除非线程由于密集的同步需求而被持续阻塞,否则非公平信号量吞吐量更好
避免性能测试的陷阱(陷阱使性能测试毫无意义) 1.垃圾回收:执行时序不定,可能对最终测试的每次迭代时间有很大影响 2.如何防止垃圾回收操作对测试结果产生偏差:A.确保垃圾回收操作在测试运行期间不执行 B.确保垃圾回收操作在测试期间执行多次,充分反映运行期间内存分配与垃圾回收等开销 3.动态编译:A.过程:类第一次加载,JVM通过解释字节码方式执行。若一个方法运行次数足够多,动态编译器会将它编译为机器代码,执行方式从解释执行变成直接执行 B.动态编译执行时机:无法预测,所有代码编译完,才应该统计测试运行时间 4.编译器在测试运行期对测试结果带来的偏差:A.编译执行的开始时间不同会对操作执行时间产生影响 B.测量代码既包含解释执行代码又包含编译执行代码,则性能指标没太大意义 5.防止动态编译对测试产生偏差:A.使程序运行时间足够长,编译过程及解释执行为总运行时间一小部分B.代码预先运行一段时间且不测试此时间内代码性能,则开始前代码完全编译 6.验证方法有效性:A.同一个JVM中相同测试运行多次,第一组结果作为“预先执行”丢弃,剩余结果存在不一致地方 B.JVM会用不同的后台线程执行辅助任务,测试不相关计算密集型操作,最好在不同操作的测试间插入显式暂停,使JVM与后台任务保持一致,降低干扰 7.对代码路径不真实采样:A.JVM可对执行过程特定信息生成更优代码(编译某个程序方法M生成的代码可能与编译另一程序方法M生成的代码不同) B.即使想测试单线程性能,也应将单线程性能测试与多线程性能测试结合 8.不真实的竞争程度:A.并发程序交替执行的工作:访问共享数据以及执行线程本地计算(不同的竞争程度表现不同的性能与可伸缩性) B.并发性能测试应尽量模拟应用程序线程本地计算及并发协调开销。若测试程序中执行工作不同,测试出的性能瓶颈将不准确 9.无用代码(不会对输出结果产生任务影响)的消除:A.基础测试通常不会执行任务计算,很容易在编译器优化过程中被消除,得到虚假测试数据 B.HotSpot中应选-server模式而非-client模式,保证测试程序不会当作无用代码消除优化 C如何防止消除:要求每个计算结果都要通过某种方式来使用,但不需要同步或大量计算,且计算结果应不可预测(否则智能的动态优化编译器将用预先计算结果代替)
其他测试方法(目的:不是发现更多错误,而是提高代码能按预测方式工作的可信度) 1.代码审查:代码编写者和其他人来审查并发代码,提高细节注释质量,降低后期维护成本 2.静态分析工具:能生成警告列表,警告信息必须通过手工检查,会有少部分伪警告 3.检查器可发现以下并发相关错误:A.不一致同步B.直接调用Thread.run(实现Runnable才可调用) C.未被释放的锁(控制流退出显示锁,通常不会自动释放)D.空同步块E.双重检查加锁F.在构造函数中启动一个线程(This引用从构造函数中逸出) G.通知错误H.条件等待中的错误I.对Lock和Condition的误用(Lock作为同步块通常是错误用法)J.在休眠或等待的同时持有一个锁K.自旋循环(除了自旋检查某个域值外不做任何事情) 4.面向方面测试技术(AOP):可用来确保不变形条件不被破坏,与同步策略某些方面保持一致,但在并发领域应用有限,主流AOP工具不支持在同步位置处的“切入点” 5.分析与检测工具:A能给出对程序内部的详细信息(侵入式,会对程序执行时序和行为产生极大影响)B.为每个线程提供一个时间线显示,用颜色区分不同的线程状态 C.可看出:程序对CPU资源利用率,哪些锁导致竞争,程序表现糟糕应从何处查原因 6.内置的JMX代理(提供有限的功能监测线程的行为):A.ThreadInfo类包含线程当前状态,当线程被阻塞,它还会包含发生阻塞所在的锁或条件队列B.若启用“线程竞争监测”,ThreadInfo中还会包括由于等待一个锁或通知而被阻塞的次数,以及累计等待时间