详解Java并发的性能与可伸缩性
欢迎Java工程师关注简书专栏
Java架构技术进阶
本专栏收录各种Java相关技术,面试题,以及学习感悟,心得!
线程最主要的目的是提高系统的运行性能。
始终要把安全性放在第一位,首先保证线程的安全性,只有在程序有性能要求时,才进行进一步的性能优化。
一、性能与可伸缩性
对于服务器应用程序来说,“多少”这个方面比“多块”这个方面更受重视。
当进行决策时,有时候会通过增加某种形式的成本来降低另一种形式的开销。
1.1 性能
当进行性能调优时,其目的是用更小的代价完成相同的工作。
1.1.1 性能优化
要提高应用的性能,主要有两方面:
- 有效利用现有的处理资源
- 在出现新的处理资源时使系统尽可能的利用这些资源。
1.1.2 指标
程序的性能可以用多个指标衡量:
- 程序运行速度
服务时间,响应时间,等待时间 - 程序处理能力
生产量,吞吐量
1.2 可伸缩性
可伸缩性指,当增加计算资源时(CPU、内存、IO、带宽)时,程序的吞吐量或处理能力能相应的增加。
1.2.1 可伸缩性优化
目的是怎么将问题并行化。
二、Amdahl定律
在增加计算资源的情况下,程序在理论上能够实现最高的加速比,这个值取决于程序中可并行组件与串行组件所占的比例。
在所有的并发程序中都包含一些串行部分。
三、线程引入的开销
并行带来的性能提升必须超过并发导致的开销。
3.1 上下文切换
- 上下文切换的开销
- 缓存丢失的开销
当线程频繁的发生阻塞,那么它们将无法使用完整的调度时间片。
3.2 内存同步
内存同步一般都会使用一些特殊的命令,即内存栅栏。内存栅栏可以刷新缓存,使缓存无效,刷新硬件的写缓存,以及停止执行管道,它将抑制编译器及硬件的优化操作。
同步会加大内存总线上的通信量。
3.3 阻塞
竞争的同步需要操作系统的介入,增加开销。
- 自旋等待
通过循环的尝试获取锁,直到成功 - 操作系统挂起
由操作系统将线程移到等待队列。
四、减少锁竞争
在并发程序中,对可伸缩性的最主要的威胁就是独占方式的资源锁。
4.1 原则
原则:
- 降低锁的获取频率
- 降低持有锁的时间
如果两者的乘积很小,那么大多数锁的操作都不会发生竞争,因此在该锁上的竞争不会对可伸缩性造成严重的影响。 - 使用带有协调机制的独占锁
4.2 策略
4.2.1 缩小锁的范围
尽可能的缩短锁持有的时间。
4.2.2 减少锁的粒度
减少锁的请求频率,这可以通过锁分解和锁分段等技术实现,在这些技术中将采用多个相互独立的锁来保护独立的状态变量,从而改变这些变量在之前由单个锁保护的情况。
4.2.2.1 锁分解
如果一个锁需要保护多个相互独立的状态变量,那么可以将这个锁分解为多个锁,并且每一个锁只保护一个变量,从而提供可伸缩性,最终降低每个锁被请求的频率。
4.2.2.2 锁分段
可以将锁分解技术进一步扩展为对一组独立对象上的锁进行分解,这种情况被称为锁分段。
ConcurrentHashMap
4.2.3 避免热点域
4.2.4 替代独占锁的其他方法
4.2.4.1 ReadWriteLock
针对读多写少得情况进行了进一步的优化。
4.2.4.2 原子变量
可以降低热点域的开销
4.2.5 拒绝对象池
线程分配新的对象时,基本上不需要在线程间进行协调,直接从线程本地内存块进行分配即可。
通常,对象分配操作的开销比同步开销更低。
4.2.6 监控CPU利用率
- 负载不充足
- IO密集型
- 外边限制
- 锁竞争
五、减少上下文切换开销
日志记录的例子:
如果直接由日志的操作线程进行日志的记录,会执行写得IO操作,从而发生阻塞,进行一次线程上下文切换。
如果将IO操作从处理请求的线程中分离出来,处理线程只需将日志对象添加到队列中即可,从而避免线程切换。