您的位置:首页 > 编程语言

并发编程实战学习笔记(八)——性能与可伸缩性

2017-03-26 16:43 615 查看

性能追求与安全性复杂性的矛盾

许多提升性能的技术同样会增加复杂性,因此也就增加了在安全性和活跃性上发生失败的风险。

并发程序设计的最基本原则

首先要保证程序能正确运行,然后仅当程序的性能需求和测试结果要求程序执行得更快时,才应该设法提高它的运行速度。

提高程序运行速度的总体思路

先使程序正确运行,首先可以从代码内在逻辑层面思考优化点外,通过压测等手段来观察系统资源使用瓶颈,从而实现通过实验数据而不是主观猜想来提出新的策略。

应用程序性能的衡量指标

服务时间、等待时间用于衡量程序的“运行速度”,即某个指定的任务单元需要“多快”才能处理完成。

生产量、吞吐量用于衡量程序的“处理能力”,即在给定计算机资源的情况下,能完成“多少”工作。

因此得出结论:性能的提高就是使应用程序,1)对任务单元的处理速度更快,2)资源一定的情况下,完成更多的工作

可伸缩性定义

当增加计算资源时(例如CPU、内存、存储容量或I/O带宽),程序的吞吐量或者处理能力能相应地增加。

对于用户访问量快速增加的服务端程序而言,良好的可伸缩性是至关重要的,直接决定了能否在QPS快速增加的时候,简单的通过添加计算资源就能承受住访问压力,并且正常提供服务。否则只能是拒绝服务,或者响应速度大大降低了。

良言相劝

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

不要过度担心非竞争同步带来的开销。这个基本的机制已经非常快了,并且JVM还能进行额外的优化以进一步降低或者消除开销。因此,我们应该将优化重点放在那些发生锁竞争的地方。

Amdahl定律

在增加计算资源的情况下,程序在理论上能够实现最高加速比,这个值取决于可并行组件与串行组件所占的比重。假定F是必须被串行执行的部分,那么根据Amdahl定律,在包含N个处理器的机器中,最高的加速比为:

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

当N趋近于无穷大时,最大的加速比趋近于1/F。因此,如果程序有50%的计算需要串行执行,那么最高的加速比只能是2(而不管有多少个线程可用)

我们评估一个算法时,要考虑算法在数百个或数千个处理器的情况下的性能表现,从而对可能出现的可伸缩性局限有一定程度的认识。

自旋锁的使用场景

如果等待时间较短,则适合采用自旋等待方式,如果等待时间较长,则适合采用线程挂起方式。

降低锁竞争的理论分析

在并发程序中,对可伸缩性的最主要威胁就是独占方式的资源锁。

有两个因素将影响在锁上发生竞争的可能性:锁的请求频率,以及每次持有该锁的时间。

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

- 减少锁的持有时间

- 降低锁的请求频率

- 使用带有协调机制的独占锁,这些机制允许更高的并发性。比如读写锁

减少锁竞争的方法

缩小锁的范围(快进快出)。

能锁代码块就不要锁整个对象,能锁对象就不要锁整个类等等;

细粒度锁之锁分解。

比如一开始是锁整个对象,将对象里边不需要原子操作的多个域分开设置锁,获取不同的锁,操作不同的域,这样就降低了对单个锁的竞争激烈程序。

细粒度锁之锁分段。

参考ConcurrentHashMap里边将EntrySet数组分成16个分段锁,从而大大降低了锁的竞争

避免热点域。

参考ConcurrentHashMap里边将热点域size分成多个值,当我们需要获取全局size的时候,就临时把这些值加起来就是,虽然可能得不到一个准确的值,但大大提高了并发性,是划算的。

使用一些替代独占锁的方法

放弃使用独占锁,从而有助于使用一种友好并发的方式来管理共享状态。例如,使用并发容器、读-写锁、不可变对象以及原子变量。

监测CPU的利用率从而分析出系统瓶颈

如果CPU没有得到充分利用,那么需要找出其中的原因(vmstat,mpstat查询CPU使用情况)。可能的原因如下:

负载不充足。可以在测试时增加负载,并检查利用率,响应时间和服务时间等指标的变化。如果产生足够多的负载使应用程序达到饱和,那么可能需要大量的计算机能耗,并且问题可能在于客户端系统是否具有足够的能力,而不是被测试系统。

IO密集。可以通过iostat或者perfmon来判断某个应用程序是否是磁盘I/O密集型的,或者通过监测应用的通信流量来判断它是否需要高带宽。

外部限制。如果应用程序依赖于外部服务,比如数据库或web服务,那么性能瓶颈可能并不在你自己的代码中。

锁竞争。使用分析工具可以知道在程序中存在何种程度的锁竞争。比如进行线程栈帧转储,来观察是不是有“waiting to lock monitor”之类的关键字。

在CPU保持忙碌状态之后,我们试试增加CPU的数量,比如从4核换到8核,看是否能增加处理能力,如此就可以得出结论:增加CPU可以提高程序的处理能力,类似的其它资源验证过程也是类似的。

向对象池说“不”

早期垃圾回收机制很慢,效率很低,很多程序通过对象池来降低垃圾回收的压力。但现在的垃圾回收机制已经很快了。在并发程序中,对象池的表现更加糟糕:

如果这些线程从对象池中请求一个对象,那么就需要通过某种同步来协调对对象池数据结构的访问,从而可能使某个线程被阻塞。

如果某个线程由于锁竞争而被阻塞,那么这种阻塞的开销将是内存分配操作开销的数百倍,因此即使对象池带来的竞争很小,也可能形成一个可伸缩性的瓶颈。(即使是一个非竞争的同步,导致的开销也会比分配一个对象的开销大。)

虽然这看似是一种性能优化技术,但实际上却会导致可伸缩性问题。

传统网络模式下,同步阻塞IO将导致上下文切换,同时,一个连接一个线程将导致更多的上下文切换,改进方法如下

将阻塞IO操作从处理请求的线程分离出来,放到专门的线程中去处理。

使用nio,多路复用机制,实现可以由有限线程池来处理所有的连接请求。

使用用nio,异步io,详细参考:聊聊Linux 五种IO模型
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  并发 性能