线程最主要的目的就是提高程序的运行性能。
提升性能意味着用更少的资源做更多的事。当操作性能由于某种特定的资源而受到限制时,通常将该操作称为资源密集型的操作,如CPU密集型、数据库密集型。
造成性能开销的操作包括:线程之间的协调(如加锁,触发信号以及内存同步等),增加的上下文切换,线程的创建和销毁,以及线程的调度等。
要想通过并发来获得更好的性能,需要努力做好两件事情:
可伸缩性指的是:当增加计算资源时(如CPU、内存、存储容量或I/O带宽),程序的吞吐量或者处理能力能相应地增加。
避免不成熟的优化,首先使程序正确,然后再提高运行速度——如果它还运行得不够快。
以测试为基准,不要猜测。
在使某个方案比其他方案”更快”之前,首先问自己一些问题:
Amdahl定律描述的是:在增加计算机资源的情况下,程序在理论上能够实现最高加速比,这个值取决于程序中可并行组件于串行组件所占的比重。假定F是必须被串行执行的部分,根据定律,在包含N个处理器的机器中,最高的加速比为:
Speedup<=1/(F+(1-F)/N)
当N接近无穷大时,最大的加速比接近于1/F。
在所有并发程序中都包含一些串行部分。如果你认为你的程序中不存在串行部分,那么可以在仔细检查一遍。
对于为了提升性能而引入的线程来说,并行带来的性能提升必须超过并发导致的开销。
如果可运行的线程数大于CPU的数量,那么操作系统最终会将某个正在运行的线程调度出来,从而使其它线程能够使用CPU。这将导致一次上下文切换。在这个过程中将保存当前运行线程的执行上下文,并将新调度进来的线程的执行上下文设置为当前上下文。
按照经验来看:在大多数通用的处理器中,上下文切换的开销相当于5000~10000个时钟周期,也就是几微妙。如果内核占用率较高(超过%10),那么通常表示调度活动发生得很频繁,这很可能是由I/O或竞争锁导致的阻塞引起的。
在评估同步操作带来的性能影响时,区分有竞争的同步和无竞争的同步非常重要。synchronized机制针对无竞争的同步进行了优化(volatile通常是非竞争的)。
不要过度担心非竞争同步带来的开销。这个基本的机制已经非常快了,并且JVM还能进行额外的优化以进一步降低或消除开销。因此,我们应该将优化重点放在那些发生锁竞争的地方。
JVM在实现阻塞行为时,采用自旋等待(Spin-Waiting,指通过循环不断地尝试获取锁,直到成功)或者通过操作系统挂起被阻塞的线程。如果等待时间较短,适合采用自旋等待,如果等待时间较长,适合采用线程挂起方式。
在并发程序中,对可伸缩性的最主要威胁就是独占方式的资源锁。
有两个因素将影响在锁上发生竞争的可能性:锁的请求频率,以及每次持有该锁的时间。
有3中方式可以降低锁的竞争程度:
通过缩小锁的作用范围。JVM执行锁粒度粗话操作,可能会将分解的同步块又重新合并起来,仅当可以将一些”大量”的计算或阻塞操作从同步代码块中移出时,才应该考虑同步代码块的大小。
降低线程请求锁的频率。可以通过锁分解和锁分段来实现。
如果一个锁需要保护多个相对独立的状态遍历,那么可以将这个锁分解为多个锁,并且每个锁只保护一个变量,从而提高可伸缩性,并最终降低每个锁被请求的频率。
如果在锁上存在适中而不是激烈的竞争时,通过将一个锁分解为两个锁,能最大限度地提升性能。
将锁分解技术进一步扩展为对一组独立对象上的锁进行分解,这种情况被称为锁分段。例如,在ConcurrentHashMap的实现中使用了一个包含16个锁的数组,每个锁保护所有散列桶的1/16,还可以进一步增加锁的数量。
锁分段的一个劣势在于:与采用单个锁实现独占访问相比,要获取多个锁来实现独占访问将更加困难并且开销更高。
//在基于散列的Map中使用锁分段技术
@ThreadSafe
public class StripedMap{
//同步策咯:buckets[n] 由lock[n%N_LOCKS]来保护
private static final int N_LOCKS = 16;
private final Node[] buckets;
private final Object[] locks;
private static class Node{...}
public StripedMap(int numBuckets){
buckets = new Node[numBuckets];
locks = new Object[N_LOCKS];
for(int i=0;i
避免热点域
当每个操作都请求多个变量时,锁的粒度将很难降低。”热点域”往往会限制可伸缩性。
即使使用锁分段技术来实现散列链,那么在对计数器的访问进行同步时,也会重新导致在使用独占锁时存在的可伸缩性问题。计数器被称为”热点域”。
为了避免这个问题,ConcurrentHashMap中的size将对每个分段进行枚举并将每个分段中的元素数量相加,而不是维护一个全局计数。为了避免枚举每个元素,ConcuttentHashMap为每个分段都维护了一个独立的计数,并通过每个分段的锁来维护这个值。
一些替代独占锁的方法
- 放弃使用独占锁,对于只读的数据结构,其中包含的不变性可以完全不需要加锁操作。
- 原子变量提过了一种方式来降低更新”热点域”时的开销。原子变量类提供了在整数或者对象引用上的细粒度原子操作(因此可伸缩性更高),并使用了现代处理器中提供的底层并发原语(例如立交并交换)。
检查CPU的利用率
如果CPU没有得到充分利用,那么需要找出其中的原因。通常有以下几个原因:
- 负载不充足
- I/O密集:是否需要高宽带
- 外部限制:如果应用程序依赖外部服务,如数据库或web服务,那么性能瓶颈可能并不子你自己的代码中。
- 锁竞争
对对象池说”不”
通常,对象分配操作的开销比同步的开销更低。
减少上下文切换
通过将I/O操作从处理请求的线程中分离出来,可以缩短处理请求的平均服务时间。
小结
通常可以通过以下方式来提升可伸缩性:
- 减少锁的持有时间。
- 降低锁的粒度。
- 采用非独占的锁或非阻塞锁来代替独占锁。