第10章 避免活跃性危险
10.1 死锁
-10.1.1 锁顺序死锁
最简单的一种死锁形式:
-10.1.2 动态的锁顺序死锁
可以通过下面的方法来解决:
-10.1.3 在协作对象之间发生死锁
-10.1.4 开放调用
如果在调用某个方法时不需要持有锁,那么这种调用就被称为开放调用。
-10.1.5 资源死锁
当多个线程在相同资源上等待时,也会发生死锁。
10.2 死锁的避免与诊断
-10.2.1 支持定时的锁
显示使用Lock类中的定时tryLock功能(见13章)来代替内置锁机制。
-10.2 通过线程转储信息来分析死锁
10.3 其他活跃性危险
-10.3.1 饥饿
当线程由于无法访问它所需要的资源而不能继续执行时,就发生了”饥饿”。引发饥饿最常见的资源就是CPU时钟周期。
-10.3.2 糟糕的响应性
-10.3.3 活锁
这种问题虽然不会阻塞线程,但也不能继续执行,因为线程将不断重复执行相同操作,而且总会失败。活锁通常发生在处理事务消息的应用程序中:如果不能成功处理某个消息,那么消息处理机制将回滚整个事务,并将它重新放到队列的开头。如果消息处理器在处理某种特定类型的消息时存在错误并导致它失败,那么每当这个消息从队列中取出并传递到存在错误的处理器时,都会发生事务回滚。由于这条消息又被放到队列开头,因此处理器将被反复调用,并返回相同结果。虽然处理消息的线程并没有阻塞,但也无法继续执行下去。要解决这种问题,需要在重试机制中引入随机性。
第11章 性能与可伸缩性
11.1 对性能的思考
11.1.1 性能与可伸缩性
11.1.2 评估各种性能权衡因素
11.2 Amdahl定律
在增加计算机资源的情况下,程序在理论上能够实现最高加速比,这个值取决于可并行组件与串行组件所占的比重。假定F是必须被串行执行的部分,那么根据Amdahl定律,在包含N个处理器的机器中,最高的加速比是:
要预测应用程序在某个多处理器系统中将实现多大的加速比,还需要找出任务中的串行部分。
11.2.1 示例:在各种框架中隐藏的串行部分
11.3 线程引入的开销
11.3.1 上下文切换
JVM和操作系统的开销。除此之外,当一个新的线程被切换进来时,它所需要的数据可能不在当前处理器的本地缓存中因此上下文切换将导致一些缓存缺失,因而线程在首次调度运行时会更加缓慢。
11.3.2 内存同步
内存栅栏(Memory Barrier)。内存栅栏可以刷新缓存,使缓存无效,刷新硬件的写缓冲,以及停止执行管道。现代的JVM能通过优化来去掉一些不会发生竞争的锁,从而减少不必要的同步开销。一些更完备的JVM能通过逸出分析(Escape Analysis)来找出不会发布到堆的本地对象引用。即使不进行逸出分析,编译器也可以执行锁粒度粗化(Lock Coarsening)将邻近的同步代码块用一个锁合并起来。
11.3.3 阻塞
JVM在实现阻塞行为时,可以采用自旋等待(通过循环不断地尝试获取锁,知道成功)或者通过操作系统挂起被阻塞的线程。
11.4 减少锁的持有时间
11.4.1 缩小锁的范围
来个例子:
由于在AttributeStore中只有一个状态变量attributes,因此可以通过将线程安全性委托给其他的类来进一步提升它的性能。通过用线程安全的Map(Hashtable,SynchronousMap或ConcurrentHashMap)来代替attributes, AttributeStore可以将确保线程安全性的任务委托给顶层的线程安全容器来实现。
11.4.2 减小锁的粒度
降低线程请求锁的频率,可以通过锁分解和锁分段等技术来实现。采用相互独立的锁来保护独立的状态变量。虽然能减小锁操作的粒度,实现更高的伸缩性,然而,使用的锁越多发生死锁的风险也就越高。如果一个锁要保护多个相互独立的变量,那么可以将这个锁分解为多个锁,并且每个锁只保护一个变量,从而提高可伸缩性。来个例子:
将上面的代码改一下,不用ServerStatus锁来保护用户状态和查血状态,而是每个状态都通过一个锁来保护,如下面的程序所示:
11.4.3 锁分段
在某些情况下,可以将锁分解技术进一步扩展为对一组独立对象上的锁分解,这中情况被称为锁分段。
(看起来很牛逼的样子)。锁分段的一个劣势在于:与采用单个锁来实现独占访问相比,要获取多个锁来实现独占访问将更加困难且开销更高。
11.4.4 避免热点域
锁分解和锁分段都能使不同的线程在不同的数据(或者同一个数据的不同部分)上操作,而不会相互干扰。如果程序采用锁分段技术,那么一定要表现出在锁上的竞争频率高于在锁保护的数据上发生竞争的频率。
当每个操作都请求多个变量时,锁的粒度将很难降低。这是在性能和可伸缩性之间的相互制衡的另一个方面,一些常见的优化措施,例如将一些反复计算的结果缓存起来,都会引入一些“热点域”,而这些热点域往往会限制可伸缩性。
11.4.5 一些替代独占锁的方法
第三种降低竞争锁的影响的技术就是放弃使用独占锁,从而有助于使用一种友好并发的方式来管理共享状态。例如使用并发容器,读-写锁,不可变对象以及原子变量。
11.4.6 检测cpu的利用率
11.4.7 向对象池说“不”
11.5 示例:比较Map的性能
在单线程环境下,ConcurrentHashMap的性能比同步的HashMap的性能略好一点,但在并发环境中则要好得多。在ConcurrentHashMap的实现中假设,大多是常用的操作都是获取某个已经存在的值,因此它对各种get操作进行了优化从而提供最高的性能和并发性。在同步的Map的实现中,可伸缩性的最主要阻碍在于整个Map中只有一个锁,因此每次只能有一个线程能够访问这个Map。不同的是,ConcurrentHashMap对大多数读操作并不会加锁,并且在写入操作以及其他一些需要锁的读操作中使用了锁分段技术。
11.6 减少上下文切换的开销
当任务在运行和阻塞这两个状态之间转换时,就相当于一个上下问切换。在服务器应用程序中,发生阻塞的原因之一就是在处理请求时产生的各种日志消息。为了说明如何通过减少上下文切换的次数来提高吞吐量,我们将对两种日志方法的调度进行分析。(这部分没看明白在分析什么….汗…. 先pass掉)