《JAVA并发编程实践》学习笔记(第十.十一章)

来源:互联网 发布:网络文件系统搭建 编辑:程序博客网 时间:2024/05/22 02:15

第10章 避免活跃度危险

10.1 死锁

       当一个线程永远占有一个锁,而其他线程尝试去获得这个锁,那么它们将永远被阻塞。

       与其他并发危险相同,死锁很少被立即发现。一个类如果有发生死锁的潜在可能并不意味着死锁每次都发生,它只发生在改发生的时候。当死锁出现的时候,往往是最不幸的时候——在高负载之下

10.1.1 锁顺序死锁

       如果所有线程一通用的固定秩序获得锁,程序就不会出现锁顺序死锁问题了。

10.1.2 动态的锁顺序死锁

10.1.3 协作对象间的死锁

       在持有锁的时候调用外部方法是在挑战活跃度问题。外部方法可能会获得其他锁(产生死锁的风险),或者遭遇严重超时的阻塞。当你持有锁的时候会延迟其他视图获得该锁的线程。

10.1.4 开放调用

       在程序中尽量使用开放调用。依赖于开放调用的程序,相比于那些在持有锁的时候还调用外部方法的程序,更容易进行死锁自由度(deadlock-freedom)的分析。

10.1.5 死锁资源

       另外一种形式的基于资源的死锁是线程饥饿死锁(thread-starvation deadlock)。需要等待其他任务的结果的任务是生成线程饥饿死锁的来源;有界池和相互依赖的任务不能放在一起使用。

 

10.2 避免和诊断死锁

10.2.1 尝试定时的锁

       另一项检测死锁和从死锁总恢复的技术,是使用每个显示Lock类中定时tryLock特性,来代替使用内部锁机制。

10.2.2 通过线程转储分析死锁

       JVM使用线程转储(thread dump)来帮助你识别死锁的发生。线程转储包括每个运行中线程的栈追踪信息,以及与之相似并随之发生的异常。

10.3 其他的活跃度危险

       除了死锁,并发程序中任然可能遇到一些其他的活跃度危险:饥饿,丢失信号和活锁。

10.3.1 饥饿

       当线程访问它所需要的资源时却被永久拒绝,以至于不能再继续进行,这样就发生了饥饿(stravation);最常见的引发饥饿的资源是CPU周期。

       抵制使用线程优先级的诱惑,因为这会增加平台依赖性,并且可能引起活跃度问题。大多数并发应用程序可以对多有线程使用相同的优先级。

10.3.2 弱响应性

       除饥饿以外的另一个问题是弱响应性;不良的锁管理也可能引起弱响应性。如果一个线程长时间占有一个锁,其他想要访问该容器的线程就必须等待很长时间。

10.3.3

       活锁(livelock)是线程活跃度失败的另外一种形式,尽管没有被阻塞,线程却仍然不能继续,因为它不断重试相同的操作,却总是失败。

       活锁同样发生在多个相互协作的线程间,当它们为了彼此间响应而修改了状态,使得没有一个线程能够继续前进,那么久发生了活锁。(类似于两个人让路)

       解决这些多样的活锁的一种方案就是对重试机制引入一些随机性。

 

第11章 性能和可伸缩性

       使用线程最主要的原因是提高性能。使用线程可以使程序更加充分地发挥出闲置的处理能力,从而更好地利用资源;并能够使程序出现有任务正在运行的情况下立刻开始着手处理新的任务,从而提高系统的响应性。

11.1 性能的思考

       当活动的运行因某个特定资源受阻时,我们称之为受限于改资源:受限于CPU,受限于数据库。

       为了利用并发来实现更好的性能,主要针对以下两件事情:更有效地利用我们现有的处理资源,让我们的程序尽可能地开拓更多可用的处理资源。

11.1.1 性能“遭遇”可伸缩性

       应用程序可以从多个角度来衡量:服务时间,等待时间,吞吐量,效率,可伸缩性,生产量。

       可伸缩性指的是:当增加计算资源的时候(比如增加额外CPU数量、内存、存储器、I/O带宽)吞吐量和生产量能够相应地得以改进。

11.1.2 对性能的权衡进行评估

       大多数优化都不成熟的原因之一:他们通常在获得清晰的需求之前进行了假设。

       避免不成熟的优化。首先使程序正确,然后再加快——如果它运行得还不够快

11.2 Amdahl定律

       Amdahl定律描述了在一个系统中,基于可并行化和串行化的组件各自锁占有的比重,程序通过获得额外的计算资源,理论上能够加速多少。

       所有的并发程序都是一些串行源,如果你认为你没有,那么去仔细检查吧。

11.3 线程引入的开销

       调度和线程内部的协调都要付出性能的开销;对于性能改进的线程来说,并行带来的性能优势必须超过并发所引入的开销

11.3.1 切换上下文

       切换上下文是要付出代价的;线程的调度需要操作OS和JVM中共享的数据结构。

       当线程因为竞争一个锁而阻塞时,JVM通常会将这个线程挂起,允许它被换出。如果线程频繁发生阻塞,那线程就不能完整使用它的调度限额了。一个线程发生越多的阻塞,与受限于CPU的程序相比,就会造成越多的上下文切换,这增加了调度的开销,并减少了吞吐量。

       经验性原则:在大多数通用的处理器中,上下文切换的开销相当于5000到10000个时钟周期,或者几微妙。

11.3.2 内存同步

       更加成熟的JVM可以使用逸出分析(escape analysis)来识别本地对象的引用并没有在堆中被暴露,并且因此成为线程本地的。

       即使没有逸出分析,编译器同样可以进行锁的粗化(lock coarsening),把临近的synchronized块用相同的锁集合起来。

       不要过分担心非竞争的同步带来的开销。基础的机制已经足够快了,在这个基础上,JVM能够进行额外的优化,大大减少或消除开销。关注那些真正发生了锁竞争的区域中性能的优化。

11.3.3 阻塞

       当锁为竞争性的时候,失败的线程(一个或多个)必然发生阻塞。JVM既能自旋等待(spin-waiting, 不断尝试获取锁,知道成功),或者在操作系统中挂起(suspending)这个被阻塞的线程。

       哪个效率更高,取决于上下文切换的开销,以及成功地获取需要等待的时间及两者的关系。自旋等待更适合短期的等待,而挂起更适合长时间等待。一些JVM基于过去等待时间的数据剖析来选择,但大多数等待锁的线程都是被挂起的

       挂起需要两次额外的上下文切换,以及OS和缓存的相关活动:阻塞的线程在它时间限额还没有到期前就被换出,稍后如果能获得锁或者等待的资源,又会在被换入。

11.4 减少锁的竞争

       并发程序中,对可伸缩性首要的威胁是独占的资源锁。

       有两个因素影响着锁的竞争性:锁被请求的频率,以及每次持有该锁的时间。

       3种方法来减少锁的竞争

1)   减少持有锁的时间;

2)   减少请求锁的频率;

3)   或者用协调机制取代独占锁,从而允许更强的并发性。

11.4.2 减小锁的粒度

       减小持有锁的时间比例的另外一种方式是让线程减少调用它的频率,可以通过分拆锁(lock splitting)分离锁(lock striping)来实现。

11.4.3 分离锁

       把一个竞争激励的锁拆分成两个,很可能形成两个竞争激烈的锁。

       拆分锁有时候可以被扩展,分成可大可小加所块的集合,并且他们归属于相互独立的对象,这样的情况就是分离锁

       分离锁的一个负面作用:对容器加锁,进行独占访问更加困难,并且更加昂贵。

11.4.4避免热点域

       分拆锁和分离锁能够改进可伸缩性,因为他们可以使不同的线程操作不同的数据(或者相同数据结构的不同部分),而不会发生相互干扰。能够从拆分锁受益的程序,通常是那些对锁的竞争普遍大于对锁的保护数据竞争的程序。

       每一操作都请求变量的时候,锁的粒度很难被降低。

11.4.5 独占锁的替代方法

       用于减少竞争带来的影响的第三方技术是提前使用独占锁,包括:并发容器-写锁不可变对象原子变量

0 0
原创粉丝点击