性能与可伸缩性 |
虽然 java.util.concurrent
努力的首要目标是使编写正确、线程安全的类更加容易,但它还有一个次要目标,就是提供可伸缩性。可伸缩性与性能完全不同,实际上,可伸缩性有时要以性能为 代价来获得。
性能是“可以快速执行此任务的程度”的评测。可伸缩性描述应用程序的吞吐量如何表现为它的工作量和可用计算资源增加。可伸缩的程序可以按比例使用更多的处 理器、内存或 I/O 带宽来处理更多个工作量。当我们在并发环境中谈论可伸缩性时,我们是在问当许多线程同时访问给定类时,这个类的执行情况。
java.util.concurrent
中的低级别类 ReentrantLock
和原子变量类的可伸缩性要比内置监视器(同步)锁定高得多。因此,使用 ReentrantLock
或原子变量类来协调共享访问的类也可能更具有可伸缩性。
Hashtable 与 ConcurrentHashMap |
作为可伸缩性的例子,ConcurrentHashMap
实现设计的可伸缩性要比其线程安全的上一代 Hashtable
的可伸缩性强得多。Hashtable
一次只允许一个线程访问 Map
;ConcurrentHashMap
允许多个读者并发执行,读者与写入者并发执行,以及一些写入者并发执行。因此,如果许多线程频繁访问共享映射,使用 ConcurrentHashMap
的总的吞吐量要比使用 Hashtable
的好。
下表大致说明了 Hashtable
和 ConcurrentHashMap
之间的可伸缩性差别。在每次运行时,N 个线程并发执行紧密循环,它们从 Hashtable
或 ConcurrentHashMap
中检索随即关键字,60% 的失败检索将执行 put()
操作,2% 的成功检索执行 remove()
操作。测试在运行 Linux 的双处理器 Xeon 系统中执行。数据显示 10,000,000 个迭代的运行时间,对于 ConcurrentHashMap
, 标准化为一个线程的情况。可以看到直到许多线程,ConcurrentHashMap
的性能仍保持可伸缩性,而 Hashtable
的性能在出现锁定竞争时几乎立即下降。
与通常的服务器应用程序相比,这个测试中的线程数看起来很少。然而,因为每个线程未进行其他操作,仅是重复地选择使用该表,所以这样可以模拟在执行一些实 际工作的情况下使用该表的大量线程的竞争。
线程 | ConcurrentHashMap | Hashtable |
---|---|---|
1 | 1.0 | 1.51 |
2 | 1.44 | 17.09 |
4 | 1.83 | 29.9 |
8 | 4.06 | 54.06 |
16 | 7.5 | 119.44 |
32 | 15.32 | 237.2
|
Lock 与 synchronized 与原子 |
下列基准说明了使用 java.util.concurrent
可能改进可伸缩性的例子。该基准将模拟旋转骰子,使用线性同余随机数生成器。有三个可用的随机数生成器的实现:一个使用同步来管理生成器的状态(单一变 量),一个使用 ReentrantLock
,另一个则使用 AtomicLong
。下图显示了在 8-way Ultrasparc3 系统上,逐渐增加线程数量时这三个版本的相对吞吐量。(该图对原子变量方法的可伸缩性描述比较保守。)
图 1. 使用同步、Lock 和 AtomicLong 的相对吞吐量
公平与不公平 |
java.util.concurrent
中许多类中的另外一个定制元素是“公平”的问题。公平锁定或公平信号是指在其中根据先进先出(FIFO)的原则给与线程锁定或信号。ReentrantLock
、Semaphore
和 ReentrantReadWriteLock
的构造函数都可以使用变量确定锁定是否公平,或者是否允许闯入(线 程获得锁定,即使它们等待的时间不是最长)。
虽然闯入锁定的想法可能有些可笑,但实际上不公平、闯入的锁定非常普遍,且通常很受欢迎。使用同步访问的内置锁定不是公平锁定(且没有办法使它们公平)。 相反,它们提供较弱的生病保证,要求所有线程最终都将获得锁定。
大多数应用程序选择(且应该选择)闯入锁定而不是公平锁定的原因是性能。在大多数情况下,完全的公平不是程序正确性的要求,真正公平的成本相当高。下表向 前面的面板中的表中添加了第四个数据集,并由一个公平锁定管理对 PRNG 状态的访问。注意闯入锁定与公平锁定之间吞吐量的巨大差别。
图 2. 使用同步、Lock、公平锁定和 AtomicLong 的相对吞吐量