锁竞争激烈的解决方法

在遇到锁竞争激烈的时候,很多开发人员会想到“我需要一个更快的锁”。实际上,某些锁实现机制异常缓慢,对于这类锁,可以使用更快的锁来代替。但是,快慢是相对的,无论锁的速度有多快,都必然会导致线程串行化,因此使用更快的锁对程序性能的提高是一个定值,对程序的可扩展性并无改善。要提高程序的可扩展性,要么不使用锁,要么化解锁的竞争。

在前面讨论死锁时,曾经提到可以通过复制资源的方式来避免使用锁。只要可行,这是一个非常好的解决方法。这样,每个线程访问自己的资源副本,从而可以避免使用锁。如果有需要的话,可以在程序的最后将各个线程占有的资源副本合并成一个单一的共享资源副本。

根据排队理论,一个资源闲置得越少,要得到它的平均等待时间就越长。这种关系是非线性的;如果锁的个数翻倍,平均等待这个锁的时间就比原来的两倍还要多。减少对锁的等待时间的最有效方法是减少这个锁所保护的范围大小。所以如果某个资源无法被复制,就可以考虑将该资源分割成若干个部分,然后用彼此独立的锁分别保护分割以后的各个部分。考虑多个线程同时向一个散列表中插入数据的情形。为了避免数据竞争,每个线程在访问散列表的时候都需要对散列表加锁。这种方案存在一个重大的缺陷,所有的线程都必须竞争同一个锁,这就形成了一个性能瓶颈。于是我们可以通过一个散列函数,将关键字映射到一个子表,并且使每个子表有一个独立的锁。对于一个给定的关键字,线程可以使用散列函数将关键字映射到一个子表,然后线程取得这个子表的锁,再对这个子表进行操作。只要有足够数量的子表和足够好的散列函数,线程之间就不会出现竞争同一个子表的锁的情况。

基于将竞争分担到多个锁的思想,出现了细粒度锁(fine-grained locking)的概念。例如,散列表一般是以桶数组(array of buckets)的方式实现,数组中每个桶都包含被映射到同一数组元素的关键字。在细粒度锁机制中,每个桶上都可能有一个锁。这样,多个线程就可以并发访问不同的桶。如果桶的数量固定,就可以直接实现。但是如果桶的数量必须不断增加,情况就比较复杂,因为进行数组大小变化时,锁将锁定整个桶数组,这就意味着只有进行当前操作的线程能够访问桶数组,其他的线程都被拒绝访问。可以使用读写锁解决这个问题。这种机制的另一个缺点是如果桶容量很小,那么锁的空间开销就会变得很显著。

如果一个数据结构读取频繁但是写入并不频繁,就可以使用读写锁(reader-writer lock)来解决竞争。读写锁可以区分写入者和读取者。多个读取者可以同时获取锁,但某一个时刻只有一个写入者可以获取锁。读取者在写入者持有锁的时候不能获取锁。

在多数访问为读访问,而写访问频率较低、持续时间也比较短的情况下,读写锁的性能最好。多个读线程与单个写线程交替进行操作,所以读线程和写线程都不会长时间阻止。

一个线程可以持有读线程锁或写线程锁,但是不能同时持有两者。

读线程和写线程将分别排入各自的队列。当线程释放写线程锁时,此刻读线程队列中的所有等待线程都将被授予读线程锁;当已释放所有读线程锁时,写线程队列中处于等待状态的下一个线程(如果存在)将被授予写线程锁,依此类推。换句话说,ReaderWriterLock 在一组读线程和一个写线程之间交替进行操作。

当写线程队列中有一个线程在等待活动读线程锁被释放时,请求新的读线程锁的线程会排入读线程队列。即使它们能和现有的读线程锁持有者共享并发访问,也不会给它们的请求授予权限;这有助于防止写入者被读取者无限期阻塞。

大多数在读写锁上获取锁的方法都采用超时值。使用超时可以避免应用程序中出现死锁。例如,某个线程可能获取了一个资源上的写线程锁,然后请求第二个资源上的读线程锁;同时,另一个线程获取了第二个资源上的写线程锁,并请求第一个资源上的读线程锁。如果不使用超时,这两个线程将出现死锁。如果超时间隔过期并且没有授予锁请求,则此方法通过引发ApplicationException 将控制返回给调用线程。线程可以捕捉此异常并确定下一步要进行的操作。

在使用细粒度锁的时候,需要提到的一点是过细粒度将增加对锁的请求和释放的频率,因而会增加额外的指令。您必须在过细和过粗粒度之间找到平衡。最佳粒度不得不通过试验找到。下图显示了锁的吞吐量和粒度之间的关系。

图:锁的吞吐量和粒度之间的关系

这个图是一个简单的双坐标轴图表。垂直轴或 y 轴表示吞吐量。水平轴或 x 轴表示粒度,沿着坐标标尺向外移动时粒度从精细到粗糙而变化。一条延长的贝尔曲线表明了粒度对吞吐量的关系。随着粒度从精细到粗糙,吞吐量逐渐增加到一个最大水平,然后开始慢慢下降。这表明为了达到最大吞吐量,必须在粒度上折衷。


Posted

in

by

Tags:

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *