在一个并发程序中,对可伸缩性的最主要威胁就是独占方式的资源锁。
影响在锁上发生竞争的可能性的两个因素:锁的请求频率和持有该锁的时间。
有三种方式可以降低锁的竞争程度:
- 减少锁的持有时间
- 降低锁的请求频率
- 使用带有协调机制的独占锁,这些机制允许更高的并发性。
1.缩小锁的范围(“快进快出”)
降低发生竞争可能性的一种有效方式就是尽可能缩短锁的持有时间。例如,可以将一些与锁无关的代码移出同步代码块,尤其是那些开销较大的操作,以及可能被阻塞的操作,例如I/O操作。
尽管缩小同步代码块能提高可伸缩性,但同步代码块也不能过小——一些需要采用原子方式执行的操作必须包含在一个同步块中。此外,同步需要一定的开销,当把一个同步代码分解为多个同步代码块时,反而会对性能提升产生负面影响。在分解同步代码块时,理想的平衡点将与平台相关,但在实际情况中,仅当可以将一些“大量”的计算或阻塞操从同步代码块中移出时,才应该考虑同步代码块的大小。
2.减小锁的粒度
另一种减小锁的持有时间的方式是降低线程请求锁的频率(从而减少发生竞争的可能性)。这可以通过锁分解和锁分段等技术来实现,在这些技术中将采用多个相互独立的锁来保护独立的状态变量,从而改变这些变量在之前有单个锁来保护的情况。这些技术能减少锁操作的粒度,并能实现更高的可伸缩性,然而,使用的锁越多,那么发生死锁的风险也就越高。
如果在锁上存在适中而不是激烈的竞争时,通过将一个锁分解为两个锁,能最大限度地提高性能。如果对竞争并不激烈的锁进行分解,那么在性能和吞吐量等方面带来的提升将非常有限,但是也会提高性能随着竞争提高而下降的拐点值。对竞争适中的锁进行分解时,实际上是把这些锁转变为非竞争的锁,从而有效地提高性能和可伸缩性。
3. 锁分段
可以将所分解技术进一步拓展为对一组独立对象上的锁进行分解。例如,在ConcurrentHashMap的实现中使用了一个包含16个锁的数组,每个锁保护所有散列桶的1/16,其中第N个散列桶由第(N mod 16)个锁来保护。假设散列函数具有合理的分布性,并且关键字能够实现均匀分布,那么这大约能把对于锁的请求减少到原来的1/16。
锁分段的一个劣势在于:与采用单个锁来实现独占访问相比,要获取多个锁来实现独占访问将更加困难并且开销更高。通常,在执行一个操作时最多只需获取一个锁,但在某些情况下需要加锁整个容器。例如当ConcurrentHashMap需要扩展映射范围,以及重新计算键值的散列值要分布到更大的桶集合时,就需要获取分段锁集合中的所有的锁。
网友评论