第11章 性能与可伸缩性

要想通过并发来获得更好的性能,需要努力做好两件事情:更有效的利用现有处理资源以及在出现新的处理资源时使程序尽可能地利用这些新资源。

第6章介绍了如何识别任务的逻辑边界并将应用程序分解为多个子任务。然而要预测应用程序在某个多处理器系统中将实现多大的加速比,还需要找出任务中的串行部分。

单个任务的处理时间不仅包括执行任务Runnable的时间,也包括从共享队列中取出任务的时间。如果使用LinkedBlockingQueue作为工作队列,那么出列操作被阻塞的可能性将小于使用同步LinkedList时发生阻塞的可能性,因为LinkedBlockingQueue使用了一种可伸缩性更高的算法。然而,无论访问何种共享数据结构,基本上都会在程序中引入一个串行部分。

这个示例还忽略了另一种常见的串行操作:对结果进行处理。所有有用的计算都会生成某种结果或者产生某种效应——如果不会,那么可以将它们作为“死亡代码”删除掉。由于Runnable没有提供明确的结果处理过程,因此这些任务一定会产生某种效果,例如将它们的结果写入到日志或者保存到某个数据结构。通常,日志文件和结果容器都会由多个工作者线程共享,并且这也是一个串行部分。如果所有线程都将各自的计算结果保存到自行维护数据结构中,并且在所有任务都执行完成后再合并所有的结果,那么这种合并操作也是一个串行部分。

当在锁上发生竞争时,竞争失败的线程肯定会阻塞。JVM在实现阻塞行为时,可以彩自旋等待(Spin-Waiting,指通过循环不断地尝试获取锁,直到成功)或者通过操作系统挂起被阻塞的线程。这两种方式的效率高低,要取决于上下文切换的开销以及在成功获取之前需要等待的时间。如果等待时间较短,则适合采用自旋等待方式,而如果等待时间较长,则适合采用线程挂起方式。有些JVM将根据对历史等待时间的分析数据在这两者之间进行选择,但是大多数JVM在等待时都只是将线程挂起。

有3种方式可以降低锁的竞争程度:

  • 减少锁的持有时间。
  • 降低锁的请求频率。
  • 使用带有协调机制 独占锁,这些机制允许更高的并发性。

降低发生竞争可能性的一种有效方式就是尽可能缩短锁的持有时间。例如,可以将一些与锁无关的代码移出同步代码块,尤其是那些开销较大的操作,以及可能被阻塞的操作,例如I/O操作。

在分解同步代码块时,理想的平衡点将与平台相关,但实际情况中,仅当可以将一些“大量”的计算或阻塞操作从同步代码块中移出时,才应该考虑同步代码块的大小。

如果CPU没有得到充分利用,那么需要找出其中的原因。通常有以下几种原因:

  • 负载不充足。
  • I/O密集。
  • 外部限制。例如数据库或Web服务。
  • 锁竞争。

在单线程环境下,ConcurrentHashMap的性能比同步的HashMap的性能略好一些,但在并发环境中则要好得多。在ConcurrentHashMap的实现中假设,大多数常用的操作都是获取某个已经存在的值,因此它对各种get操作进行了优化从而提供最高的性能和并发性。

在同步Map的实现中,可伸缩性的最主要阻碍在于整个Map中只有一个涣,因此条绞只有一个线程能够访问这个Map。不同的是,ConcurrentHashMap对于大多数读操作并不会加锁并且在写入操作以及其他一些需要锁的读操作中使用了锁分段技术。因此,多个线程能并发地访问这个Map而不会发生阻塞。

如果在大多数的锁获取操作上不存在竞争,那么并发系统就能执行得更好,因为在锁获取操作上发生竞争时将导致更多的上下文切换。在代码中造成的上下文切换次数越多,吞吐量就越低。

posted on 2015-10-23 09:02  a0000  阅读(147)  评论(0编辑  收藏  举报

导航