并发编程中的数据结构

'高性能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)平均时间)。
基本上,跳跃列表是对有序的链表增加上附加的前进链接,增加是以随机化的方式进行的,所以在列表中的查找可以快速的跳过部分列表,因此得名。所有操作都以对数随机化的时间进行。

image.png

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

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

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

有序数组

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

预取

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

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

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,657评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,662评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,143评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,732评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,837评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,036评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,126评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,868评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,315评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,641评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,773评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,470评论 4 333
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,126评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,859评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,095评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,584评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,676评论 2 351

推荐阅读更多精彩内容