有许多不错的无锁哈希表实现。通常,那些数据结构不是使用普通锁,而是使用基于CAS的操作来实现无锁。有了这个叙述,听起来我会用这篇文章来构建无锁数据结构的参数。事实并非如此,这令人惊讶。相反,这里我们将讨论普通的旧锁。
现在每个人都已加入,让我们Map
通过此简单合同实现并发版本:
由于这将是线程安全的数据结构,因此我们应该获取一些锁,然后在放置,获取或删除元素时释放它们:
另外,我们应该使用预定义数量的存储桶来初始化实现:
一种尺寸适合所有人吗?
当存储的元素数量远大于存储桶数量时会发生什么?即使存在最通用的哈希函数,我们的哈希表的性能也会大大降低。为了防止这种情况,我们需要在需要时添加更多存储桶:
借助这些抽象模板方法,以下是我们如何将新的键值对放入地图中的方法:
为了找到新元素的存储桶,我们依靠hashcode
键的。另外,我们应该计算hashcode
存储桶数的模块,以防止超出范围!
其余部分非常简单:我们获取新条目的锁,并且仅在尚不存在时才添加该条目。否则,我们将更新当前条目。在操作结束时,如果得出的结论是我们应该添加更多的存储桶,那么我们将这样做以保持哈希表的余额。该get
和remove
方法也可以实现轻松。
让我们通过实现所有这些抽象方法来填补空白!
一键统治所有
实现此数据结构的线程安全版本的最简单方法是对其所有操作仅使用一个锁:
当前的获取和发布实现完全独立于地图条目。如所承诺的,我们仅使用一个ReentrantLock
进行同步。这种方法被称为粗粒度同步,因为我们仅使用一个锁来强制对整个哈希表进行独占访问。
另外,我们应该调整散列表的大小,以保持其不变的访问和修改时间。为此,我们可以合并不同的启发式方法。例如,当平均存储桶大小超过特定数字时:
为了添加更多的存储桶,我们应该复制当前的哈希表并创建一个新表,该表的大小是原来的两倍。然后将旧表中的所有条目添加到新表中:
请注意,我们也应该获取并释放相同的锁以进行大小调整操作。
顺序瓶颈
假设存在三个并发请求,分别将某些内容放入第一个存储桶,从第三个存储桶中获取和从第六个存储桶中删除:
理想情况下,我们期望从高度可扩展的并发数据结构中,以尽可能少的协调来满足此类请求。但是,这是在现实世界中发生的事情:
由于我们仅使用一个锁进行同步,因此并发请求将在第一个获得该锁的请求之后被阻止。当第一个释放锁时,第二个获得它,一段时间后释放它。
这种现象被称为顺序瓶颈(类似于行阻塞头),我们应该在并发环境中减轻这种影响。
联锁条
改善我们当前的粗粒度实现的一种方法是使用一组锁而不是仅一个锁。这样,每个锁将负责同步一个或多个存储桶。与我们当前的方法相比,这更像是一种细粒度的同步:
在这里,我们将哈希表分为几个部分,并分别同步每个部分。这样,我们减少了每个锁的争用,因此提高了吞吐量。这个想法也称为“锁定条带化”,因为我们独立地同步了不同部分(即条带):
然后,我们可以获取并释放针对特定条目的操作的锁,如下所示:
调整大小的过程与以前几乎相同。唯一的区别是我们应该在调整大小之前获取所有锁,并在调整大小之后释放所有锁。
我们希望细粒度方法优于粗粒度方法,让我们对其进行衡量!
关键时刻
为了对这两种方法进行基准测试,我们将在两种实现方式上使用以下工作负载:
每次我们将放置一个随机密钥,获取一个随机密钥并删除一个随机密钥。每个工作负载将运行这三个操作100次。将JMH添加到组合中,每个工作负载将执行数千次:
粗粒度实现平均每秒可处理8547次操作:
另一方面,细粒度的实现平均每秒最多可处理13855次操作。
递归拆分顺序列表
Ori Shalev和Nir Shavit 在他们的论文中提出了一种实现无锁哈希表的方法。他们使用了一种对链表中的元素进行排序的方式,以便可以使用单个比较和交换操作将它们重复“拆分”。无论如何,他们使用了完全不同的方法来实现此并发数据结构。强烈建议签出并阅读该论文。
有什么问题可以加下qq:2062583349。也可添加vx:admindesire,有java、python、web等习资料和视频课程干货”。欢迎交流!