性能追求与安全性复杂性的矛盾
许多提升性能的技术同样会增加复杂性,因此也就增加了在安全性和活跃性上发生失败的风险。
并发程序设计的最基本原则
首先要保证程序能正确运行,然后仅当程序的性能需求和测试结果要求程序执行得更快时,才应该设法提高它的运行速度。
提高程序运行速度的总体思路
先使程序正确运行,首先可以从代码内在逻辑层面思考优化点外,通过压测等手段来观察系统资源使用瓶颈,从而实现通过实验数据而不是主观猜想来提出新的策略。
应用程序性能的衡量指标
- 服务时间、等待时间用于衡量程序的“运行速度”,即某个指定的任务单元需要“多快”才能处理完成。
- 生产量、吞吐量用于衡量程序的“处理能力”,即在给定计算机资源的情况下,能完成“多少”工作。
因此得出结论:性能的提高就是使应用程序,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模型