深入了解Java并发——《Java Concurrency in Practice》11.性能与可伸缩性

Posted 在咖啡里溺水的鱼

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了深入了解Java并发——《Java Concurrency in Practice》11.性能与可伸缩性相关的知识,希望对你有一定的参考价值。

本章内容介绍了并发编程中,最吸引人的性能问题。什么会导致性能问题,如何解决性能问题,我们应该在什么情况下着手解决性能问题?

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

11.1 对性能的思考

尽管使用多个线程的目标是提升整体性能,但与单线程的方法相比,使用多个线程总会引入一些额外的性能开销。

要想通过并发来获得更好的性能,需要做好两件事:
1. 更有效的利用现有处理资源
2. 在出现新的处理资源时使程序尽可能的利用这些资源

11.1.1 性能与可伸缩性

可伸缩性是指:当增加计算资源时(CPU、内存、存储容量或I/O带宽),程序的吞吐量或者处理能力能相应的增加。

再进行可伸缩性调优时,其目的是设法将问题的计算并行化,从而能利用更多的计算资源来完成更多的工作。

11.1.2 评估各种性能权衡因素

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

在大多数性能决策中都包含有多个变量,并且非常依赖与运行环境。在使某个方案比其他方案“更快”之前,首先问自己一些问题:
- 更快 的含义是什么
- 该方法在什么条件下运行的更快,低负载还是高负载、大数据集还是小数据集,能否通过测试结果来验证
- 这些条件在运行环境中发生的频率,能否通过测试结果验证
- 在其他不同条件的环境中能否使用这些代码
- 在实现这种性能提升时需要付出哪些隐含的代价,例如增加开发风险或维护开销,这种权衡是否合适

在进行任何与性能相关的决策时,都应该考虑这些问题。

以测试为基准,不要猜测

perfbar应用可以给出CPU的忙碌程度信息。

11.2 Amdahl定律

在所有并发程序中都包含一些串行部分,串行部分决定了程序性能优化的上限。

11.3 线程引入的开销

多个线程的调度和协调过程中都需要一定的性能开销:对于为了提升性能而引入的线程来说,并行带来的性能提升必须超过并发导致的开销。

11.3.1 上下文切换

UNIX系统的vmsate命令和Windows系统的perfmon工具都能报告上下文切换次数以及在内存中执行时间所占比例等信息。

11.3.2 内存同步

在评估同步操作带来的性能影响时,区分有竞争同步还是无竞争同步非常重要,synchronized机制针对无竞争的同步进行了优化(volatile通常是非竞争的)。

现代的JVM能够通过优化来去掉一些不会发生竞争的锁。

不要过度担心非竞争同步带来的开销,应该将优化重点放在那些发生锁竞争的地方。

11.3.3 阻塞

JVM在实现阻塞行为时,可以采用自旋等待Spin-Waiting,通过循环不断的尝试获取锁,知道成功,或者通过操作系统挂起被阻塞的线程。这两种方式的效率的高地,取决于上下文切换的开销以及在成功获取锁之前需要等待的时间。

如果等待时间较短,适合使用自旋等待方式,如果等待时间较长,适合采用线程挂起方式。大多数JVM在等待所时都知识将线程挂起。

11.4 减少锁的竞争

串行操作会降低可伸缩性,上下文切换也会降低性能。在锁上发生竞争时将同时导致这两个问题,因此减少锁的竞争能够提高性能和可伸缩性。

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

锁的请求频率,以及每次持有该锁的事件将影响在锁上发生竞争的可能性。

有三种方式可以降低锁的竞争程度
- 减少锁的持有事件
- 降低锁的请求频率
- 使用带有协调机制的独占所,这些机制允许更高的并发性

11.4.1 缩小锁的范围 - 快进快出

在保证原子性以及不会因为原子性而产生额外开销的前提下,缩小同步代码块能够提高可伸缩性。

11.4.2 减小锁的粒度

如果一个锁需要保护多个相互独立的状态变量,那么可以将这个锁分解为多个锁,并且每个锁只保护一个变量,从而提高可伸缩性,并最终降低每个锁被请求的频率。

11.4.3 锁分段

在某些情况下,可以将锁分解技术进一步扩展为对一组独立对象上的锁进行分解,这种情况被称为锁分段。

锁分段的劣势在于:与采用单个锁来实现独占访问相比,要获取多个锁来实现独占访问将更加困难并且开销更高。

11.4.4 避免热点域

当每个操作都请求多个变量时,锁的粒度将很难降低。这是在性能与可伸缩性之间相互制衡的另一个方面,一些常见的优化措施都会引入一些 热点域 HotField,这些热点域往往回限制可伸缩性

11.4.5 一些替代独占锁的方法

放弃使用独占所,有助于使用一种友好的并发方式来管理共享状态。

ReadWriteLock实现了一种在多个读取操作以及单个写入操作情况下的加锁规则:如果多个读取操作都不会修改共享资源,那么这些读取操作可以同时访问该共享资源,但在执行写入操作时必须以独占方式来获取锁。对于读取操作占多数的数据结构,ReadWriteLock能提供比独占锁更高的并发性。而对于只读的数据结构,其中包含的不可变性可以完全不需要任何加锁操作。

原子变量提供了一种方式来降低更新 热点域 时的开销,例如静态计数器、序列发生器、或者对链表数据结构中头结点的引用。原子变量类提供了在整数或者对象引用上的细粒度原子操作,因此可伸缩性更高,并使用了现代处理器中提供的底层并发原语。如果在类中值包含少量的热点域,并且这些域不会与其他变量参与到不变形条件中,那么用原子变量来替代它们能提高可伸缩性。

11.4.6

如果CPU没有得到充分利用,通常有一下几种原因:
- 负载不充足
- I/O密集
- 外部限制
- 锁竞争

11.4.7 拒绝对象池

对象池是针对于早期JVM的一种优化手段,现在已经没有使用的必要。

11.6 减少上下文切换的开销

以上是关于深入了解Java并发——《Java Concurrency in Practice》11.性能与可伸缩性的主要内容,如果未能解决你的问题,请参考以下文章

深入了解Java并发——《Java Concurrency in Practice》14.构建自定义的同步工具

基于JVM原理JMM模型和CPU缓存模型深入理解Java并发编程

深入了解Java并发——《Java Concurrency in Practice》10.避免活跃性危险

深入了解Java并发——《Java Concurrency in Practice》11.性能与可伸缩性

深入了解Java并发——《Java Concurrency in Practice》8.线程池的使用

一文让你深入了解 Java-Netty高性能高并发