java并发编程——性能和扩展性

第一部分:对性能的思考

并发编程的最主要目的是提高程序的运行性能,线程可以使程序更加充分的利用系统的可用处理能力,从而提高系统的资源利用率。然而使用多线程时也会引入额外的开销,这些开销包括:线程之间的协调(加锁,内存同步等)、增加的的上下文切换、线程的创建和销毁和线程的调度等等。如果过度以及不恰当的使用线程,这些开销甚至会抵消由于提高吞吐量、计算能力带来的性能提升。一个设计糟糕的多线程程序其性能可能比相同功能的串行程序效率还低。

提升性能意味着用更少的资源做更多的事情。资源的范围很广,比如CPU,内存,网络带宽,IO,磁盘空间等等。当操作性能由于某种特定的资源而受到限制时,我们通常称为资源密集型操作,比如CPU密集型等等。

要想通过并发来获得良好的性能,需要努力做好两件事:要有效的利用现有的资源,以及新的处理资源出现时程序能够有效的利用新增的资源。从性能监视的视角来看,CPU需要尽可能保持忙碌状态(当然这不意味着把CPU用在一些无用的计算上)。

应用程序的性能可以通过多个指标来衡量,比如一些指标(服务时间,等待时间)用来衡量运行速度,另一些指标(生产量、吞吐量)来衡量处理能力。因此,“多快”、“多少”是性能优化的两个不同方向,它们相互独立,有时甚至是互相矛盾的。因此,在优化之前必须要先确保以下几点:

第一,首先使程序正确,然后再提高运行速度,避免不成熟的优化。有些同步加锁的方法看上去似乎可以被一些“聪明”的减少同步的方法所取代,但这往往会引发并发错误,并发错误是最难追踪和消除的,这无疑于填小坑,挖大坑。

第二,确定目标,更快的含义是什么?在什么条件下运行更快?是在高负载还是低负载?是大数据量还是小数据量?对目前的状况以及目标状况都要有清晰地认识;

第三,以测试为基准,不要猜测。

第二部分:Amdahl定律介绍

在有些问题中,可用资源越多,那么问题的解决速度越快,比如收割庄稼,人越多则收割越快,有些任务是串行的,即使增加资源也不能提高速度。程序也是一样的,它由一些串行的和并行的程序组合而成,Amdahl定律描述的是:在增加计算资源的情况下,程序在理论上能够实现最高加速比,这个值取决于程序中并行组件与串行组件所占的比重。用公式表示为:

Speedup <= 1/(F+(1-F)/N)

Speedup 表示最高加速比,F表示程序中必须被串行的部分,N表示程序处理器的数量。当N趋向于无穷大时,最高的加速比倾向于1/F。如果程序中的串行程序占比10%,那么最高的加速比接近10。

要想算出程序中串行执行的比例其实很困难。但是Amdahl定律的出现却给我们提供了一些优化的思路。比如锁分段。根据Amdahl定律,随着处理器数量的增加,锁分段的效率要比独占锁和锁分解的效率高,也更能充分利用多处理器的能力。ConcurrentHashMap之所以有较高的性能就是使用了锁分段技术。

第三部分:如何提高性能

我们知道,串行程序会降低程序的可伸缩性,而线程的上下文切换也会降低性能,而在锁上等待会同时导致以上两种问题。因此,减少锁的竞争可提高程序的伸缩性和性能。有两个因素将影响在锁上发生竞争的可能性:锁得请求频率和每次持有锁的时间。两者的乘积越小则锁的竞争越小。以下将介绍几种减少锁竞争的方法:

第一,减少锁持有的时间(快进快出)。缩小锁的范围,只在需要锁操作的程序上使用锁,将锁无关的操作移出同步代码块。

第二,降低锁的请求频率。减小锁的粒度,这可以通过锁分解和锁分段技术实现。

第三,使用带有协调机制的独占锁,这些机制允许更高的并发性。比如我们可以用现有的并发容器替换掉同步容器以及使用原子类和读写锁。因为java自带的并发容器已经实现了很好的协调机制,我们可以直接使用提高效率。

最后要说的是性能优化是一个持续的、无止境的过程。因此,在实际优化过程中要有明确的目标,在确保程序安全性和正确性的基础上循序渐进的去展开,避免盲目和过度优化。