并发编程中的数据结构

'高性能C++编程分享'笔记

哈希表

散列表Hash table,也叫哈希表),是根据键(Key)而直接访问在内存存储位置的数据结构。也就是说,它通过计算一个关于键值的函数,将所需查询的数据映射到表中一个位置来访问记录,这加快了查找速度。这个映射函数称做散列函数,存放记录的数组称做散列表

它以理论上O(1)的时间复杂度横扫四海八荒无敌手。在数据量很大(百万以上)的情况下,哈希表比AVL树的查找效率更高。

BKDRHash

  // 字符串hash函数
  public static int bkdrhash(String str) {
      final int seed = 131;
      
      int hash = 0;
      
      for (int i = 0; i < str.length(); i++) {
         hash = hash * seed + (int)str.charAt(i);
      }
      
      return hash & 0x7FFFFFFF;
   }

缺点

  • 没有通用的哈希函数
  • 容易存在冲突
  • 预估容量困难,存储数据量增多导致冲突加剧,扩容需要重新哈希

如果想彻底避免以上的问题,可以考虑使用红黑树和跳表。在使用这两个数据结构的时候,不用考虑冲突和容量。

红黑树

红黑树是一二叉查找树。它的查找,插入,删除操作的时间复杂度都是O(logN)。STL中以它作为set和map的底层数据结构。红黑树相对于AVL树的好处是,它并不追求完全平衡,所以避免了AVL树每次更改所进行的大量平衡度统计计算,开销要小得多。此外,红黑树以此来换取增删节点时候旋转次数的降低,从而提高插入效率。

缺点

它将每个结点标记成红色或黑色,且在每个插入和删除操作中,都要进行一次调整,来避免其不同分支太不平衡。而这种调整是通过子树的旋转来实现的。这就出现了一个问题:其中涉及的结点可能包括被操作结点的父亲节点或者叔叔结点,往上到祖父母结点,甚至最后牵涉到根节点。因为树之间相隔任意距离的更新,都可能触及相同的结点。触及相同结点的概率随着结点所在层数的减少而增加,直到根节点。也就是说“牵一发而动全身”。
所以,红黑树不是一种并发友好的数据结构

跳表

普通链表

image.png

跳表

在计算机科学领域,跳跃链表是一种数据结构,允许快速查询一个有序连续元素的数据链表。快速查询是通过维护一个多层次的链表,且每一层链表中的元素是前一层链表元素的子集。
基于并联的链表,其效率可比拟于二叉查找树(对于大多数操作需要O(log n)平均时间)。
基本上,跳跃列表是对有序的链表增加上附加的前进链接,增加是以随机化的方式进行的,所以在列表中的查找可以快速的跳过部分列表,因此得名。所有操作都以对数随机化的时间进行。

并发编程中的数据结构_第1张图片
image.png

为了方便讨论,我们在这里用链表代替跳表。链表是一个并发友好的数据结构

链表上加锁是可以高度局部化的。首先看写的情况:插入只需要锁定两个现有结点,即即将插入节点的前驱和后继;删除只需要锁定三个节点,即被删除的结点,及其前驱和后继。下面再看读的情况,一个读线程最多只会持有两个结点的锁,那就是遍历链表的时候。我们可以采用 hand-by-hand 的加锁方法,只需要保证下一个读的节点不会在读之前被其他线程修改(如果下个结点提前被删除,那么我们就会访问到一块已经不存在的内存),所以持有当前读的结点的锁和下一个结点的锁就足够了。更多情况下我们只对当前正在读的结点感兴趣,所以不需要持有两把锁,只需要锁住当前结点就够了。因为锁粒度低,所以允许大量线程在同一链表上工作:它们只要处理链表的不同部分,就不会发生冲突。

再来看第二点,毋庸置疑的是,在多核系统里对链表进行操作的时候,进行高速缓存的同步成本很小。首先需要说明的是,在对高速缓存进行读写的时候,写的成本总是高于读的成本。因为写一个高速缓存时,需要广播其他高速缓存同步数据,而读则什么都不用做。基于这个认知,我们也可以知道,大量的写入会花费更多。那链表在这一点上表现如何呢?首先看写中删除结点的情况,需要传输的唯一数据是包含两个相邻结点的缓存,这些内存会被传输到所有其他cpu核的缓存,这些cpu会在随后访问该链表。再来看插入的情况,只需要传递三个结点的缓存,显然成本是很小的。

有序数组

假设你有一个稳定的数据集合,仅要从里面查找数据。可以考虑在程序启动的时候,对数据排序,然后在有序集合上做二分查找。这样做有以下优点:第一,数据结构和算法都实现简单;第二,效率高:仅仅需要简单的一次排序,以后的查找时间复杂度都是O(log2 n);第三点,有序数组有较高的缓存命中率,所以更高效。

预取

要提高缓存命中率,首先得了解CPU的一个能力,预取。
在内存和Cache之间,预取是由CPU根据过去访问的数据地址信息,从此开始线性地往下获取接下来的若干个内存单元,把这些未来可能访问的数据预先存入cache,从而在数据真正被用到时不会造成Cache失效。
在多级高速缓存之间,预取是当高一级缓存往低一级缓存中取数据时,除了获取目标数据所在的一个Cache line,高一级缓存会自动预取与其相邻的一个Cache Line。

通过了解预取我们可以知道,在对一个线性数据结构进行操作时,由于其中数据的内存地址都是连续的,所以要用到的数据有极大概率被装入缓存中,避免了频繁的各级cache数据交换所带来的时间惩罚。

所以有序数组 + 二分查找是缓存友好的
二分查找还有另外一个名字,折半查找,或许这个名字能更好地解释其原理。我们都知道,二分查找每一轮查找结束之后,查询范围都会在原有基础上缩小一半。所以,数据变得越来越密集,再加上有序数组的连续性,也就越容易被命中。所以,这就是有序数组+二分查找效率如此高的原因。

你可能感兴趣的:(并发编程中的数据结构)