哈希表存储模型

与Map接口中的内部Entry<K, V>接口(键值对)不同,

ConcurrentHashMap在其基础上,自己规定了内部类HashEntry<K, V>

static final class HashEntry<K,V> {final K key;final int hash;volatile V value;final HashEntry<K,V> next;HashEntry(K key, int hash, HashEntry<K,V> next, V value) {this.key = key;this.hash = hash;this.next = next;this.value = value;}@SuppressWarnings("unchecked")static final <K,V> HashEntry<K,V>[] newArray(int i) {return new HashEntry[i];}
}

该内部对象,多了两个属性:哈希值,以及next对象(其作用是遇到key值不同,但是哈希值相同的对象,在那个特定的哈希数组索引节点可以形成链表)

为了保证线程安全,ConcurrentHashMap类封装了自己的Map经典操作逻辑,get,put,remove等。

在其内部实现了Segment<K,V>类,主要属性为:

transient volatile HashEntry<K,V>[] table

规定了哈希数组(表)

内部类Segment<K,V>实现的经典方法:

HashEntry<K,V> getFirst(int hash) {HashEntry<K,V>[] tab = table;return tab[hash & (tab.length - 1)];}

总是返回哈希索引的第一个对象

V get(Object key, int hash) {if (count != 0) { // read-volatileHashEntry<K,V> e = getFirst(hash);while (e != null) {if (e.hash == hash && key.equals(e.key)) {V v = e.value;if (v != null)return v;return readValueUnderLock(e); // recheck}e = e.next;}}return null;}

循环遍历在此哈希节点上的所有对象吗,找到符合条件的为止。

 V put(K key, int hash, V value, boolean onlyIfAbsent) {lock();try {int c = count;if (c++ > threshold) // ensure capacityrehash();HashEntry<K,V>[] tab = table;int index = hash & (tab.length - 1);HashEntry<K,V> first = tab[index];HashEntry<K,V> e = first;while (e != null && (e.hash != hash || !key.equals(e.key)))e = e.next;V oldValue;if (e != null) {oldValue = e.value;if (!onlyIfAbsent)e.value = value;}else {oldValue = null;++modCount;tab[index] = new HashEntry<K,V>(key, hash, first, value);count = c; // write-volatile}return oldValue;} finally {unlock();}}

加锁,保证线程安全

若添加时key值不重复,hash不重复,直接填写哈希表。

若添加时key值重复,将会覆盖原先的键值对。

若添加时key值不重复,但hash值重复,总是将这类新加入的键值对节点设置成为该哈希索引的第一个节点。

HashEntry<K,V> first = tab[index];

上一方法中,哈希索引的first节点,在新的键值对生成时,变为新节点的下一个节点

tab[index] = new HashEntry<K,V>(key, hash, first, value);

HashEntry(K key, int hash, HashEntry<K,V> next, V value)

V remove(Object key, int hash, Object value) {lock();try {int c = count - 1;HashEntry<K,V>[] tab = table;int index = hash & (tab.length - 1);HashEntry<K,V> first = tab[index];HashEntry<K,V> e = first;while (e != null && (e.hash != hash || !key.equals(e.key)))e = e.next;V oldValue = null;if (e != null) {V v = e.value;if (value == null || value.equals(v)) {oldValue = v;// All entries following removed node can stay// in list, but all preceding ones need to be// cloned.++modCount;HashEntry<K,V> newFirst = e.next;for (HashEntry<K,V> p = first; p != e; p = p.next)newFirst = new HashEntry<K,V>(p.key, p.hash,newFirst, p.value);tab[index] = newFirst;count = c; // write-volatile}}return oldValue;} finally {unlock();}}

remove方法的最核心一点就是,删除节点后,需要重新构造在此哈希索引上的当前链表。

以哈希存储模型1为例,当删除e4后,其链表的调整顺序为:


 
以2为例,当删除e6后,

然而,在ConcurrentHashMap中,最核心的还要属内部类Segment的设计。

 static final class Segment<K,V> extends ReentrantLock implements Serializable { /** * 在本 segment 范围内,包含的 HashEntry 元素的个数* 该变量被声明为 volatile 型*/ transient volatile int count; /** * table 被更新的次数*/ transient int modCount; /** * 当 table 中包含的 HashEntry 元素的个数超过本变量值时,触发 table 的再散列*/ transient int threshold; /** * table 是由 HashEntry 对象组成的数组* 如果散列时发生碰撞,碰撞的 HashEntry 对象就以链表的形式链接成一个链表* table 数组的数组成员代表散列映射表的一个桶* 每个 table 守护整个 ConcurrentHashMap 包含桶总数的一部分* 如果并发级别为 16,table 则守护 ConcurrentHashMap 包含的桶总数的 1/16 */ transient volatile HashEntry<K,V>[] table; /** * 装载因子*/ final float loadFactor; Segment(int initialCapacity, float lf) { loadFactor = lf; setTable(HashEntry.<K,V>newArray(initialCapacity)); } /** * 设置 table 引用到这个新生成的 HashEntry 数组* 只能在持有锁或构造函数中调用本方法*/ void setTable(HashEntry<K,V>[] newTable) { // 计算临界阀值为新数组的长度与装载因子的乘积threshold = (int)(newTable.length * loadFactor); table = newTable; } /** * 根据 key 的散列值,找到 table 中对应的那个桶(table 数组的某个数组成员)*/ HashEntry<K,V> getFirst(int hash) { HashEntry<K,V>[] tab = table; // 把散列值与 table 数组长度减 1 的值相“与”,// 得到散列值对应的 table 数组的下标// 然后返回 table 数组中此下标对应的 HashEntry 元素return tab[hash & (tab.length - 1)]; } }

Segment 类继承于 ReentrantLock 类,从而使得 Segment 对象能充当锁的角色。每个 Segment 对象用来守护其(成员对象 table 中)包含的若干个桶。

table 是一个由 HashEntry 对象组成的数组。table 数组的每一个数组成员就是散列映射表的一个桶。(上文已经介绍)

count 变量是一个计数器,它表示每个 Segment 对象管理的 table 数组(若干个 HashEntry 组成的链表)包含的 HashEntry 对象的个数。每一个 Segment 对象都有一个 count 对象来表示本 Segment 中包含的 HashEntry 对象的总数。

注意,之所以在每个 Segment 对象中包含一个计数器,而不是在 ConcurrentHashMap 中使用全局的计数器,是为了当需要更新计数器时,不用锁定整个 ConcurrentHashMap。

在ConcurrentHashMap内部,使用数组的方式,对Segment对象进行维护。

final Segment<K,V>[] segments;

而数组segments的索引,也是通过特殊的哈希算法来确定。

final Segment<K,V> segmentFor(int hash) {return segments[(hash >>> segmentShift) & segmentMask];}

实现Map接口的put,get等方法,首先需要确定元素在segments中的索引,然后根据key值与哈希值确定segments[index]对象中table(哈希表)的索引。

public V put(K key, V value) {if (value == null)throw new NullPointerException();int hash = hash(key.hashCode());return segmentFor(hash).put(key, hash, value, false);}

整个ConcurrentHashMap的结构如下所示:

所以在内部类Segment的put, remove等方法中的加锁操作是针对(键的 hash 值对应的)某个具体的 Segment,锁定的是该 Segment 而不是整个 ConcurrentHashMap。因为插入键 / 值对操作只是在这个 Segment 包含的某个桶中完成,不需要锁定整个ConcurrentHashMap。此时,其他写线程对另外 15 个Segment 的加锁并不会因为当前线程对这个 Segment 的加锁而阻塞。同时,所有读线程几乎不会因本线程的加锁而阻塞(除非读线程刚好读到这个 Segment 中某个 HashEntry 的 value 域的值为 null,此时需要加锁后重新读取该值)。

相比较于 HashTable 和由同步包装器包装的 HashMap每次只能有一个线程执行读或写操作,ConcurrentHashMap 在并发访问性能上有了质的提高。在理想状态下,ConcurrentHashMap 可以支持 16 个线程执行并发写操作(如果并发级别设置为 16),及任意数量线程的读操作。

总结:

引入Segment的好处是,将N多个键值对的存储维护分摊到不同的Segment中,在不同的Segment中对哈希表进行维护,由于各个Segment操作之间隔离,能够最大限度的对键值对进行高效并发操作。

ConcurrentHashMapK, V的实现相关推荐

  1. linux怎么卸载webpack,安装webpack后,执行webpack -v命令时报错:SyntaxError: Block-sc

    安装webpack后,执行webpack -v命令时报错如下: [root@FreeServer ~]# webpack -v /usr/local/node-v4.4.7-linux-x64/lib ...

  2. 心音数据库_小V云端数据库 | 2020.9.14—2020.9.18

    桂花的芬芳 在雨后空气中弥散开来 似为湿润的情绪 赠予了一丝甜蜜 小V云端数据库 2020.9.14-2020.9.18 资讯情报关键词 健康.示范.安全 V宝体检,助力成长 2020年9月14日上午 ...

  3. 女士细线毛衣起多少针_潇洒帅气的男童V领开襟毛衣编织,带教程图解

    终于又完工一件. 用线:宝宝毛9号8团多用针:ADD2.75,3.0成衣尺寸:衣长43cm,胸围72cm,袖长42cm,肩宽32cm编织过程:1,用另线起针法起222针,织双螺纹5cm,排花,织20c ...

  4. 【实用】几个实用的webstorm、IDEA编辑器窗口快捷键设置,Alt+V垂直复制当前窗口,Alt+Shift+V将当前窗口复制到另一边的分割窗口显示,Alt+Shift+M移动当前活动窗口到另一边

    Alt+V垂直复制当前窗口 Alt+Shift+V将当前窗口复制到另一边的分割窗口显示,Alt+Shift+M移动当前活动窗口到另一边

  5. 基于持久内存的 单机上亿(128B)QPS -- 持久化 k/v 存储引擎

    文章目录 性能数据 设计背景 设计架构 Hash 索引结构 及 PMEM空间管理形态 基本API 及 实现 API 初始化流程 写流程 读流程 删除流程 PMEM Allocator设计 主要组件 空 ...

  6. KVell 单机k/v引擎:用最少的CPU 来调度Nvme的极致性能

    文章目录 前言 KVell背景 业界引擎使用Nvme的问题 CPU 会是 LSM-kv 存储的瓶颈 CPU 也会是 Btree-kv 存储的瓶颈 KVell 设计亮点 及 总体架构实现 KVell 设 ...

  7. 存储引擎 K/V 分离下的index回写问题

    前言 近期在做on nvme hash引擎相关的事情,对于非全序的数据集的存储需求,相比于我们传统的LSM或者B-tree的数据结构来说 能够减少很多维护全序上的计算/存储资源.当然我们要保证hash ...

  8. linux进程间通信:system V 信号量 生产者和消费者模型编程案例

    生产者和消费者模型: 有若干个缓冲区,生产者不断向里填数据,消费者不断从中取数据 两者不冲突的前提: 缓冲区有若干个,且是固定大小,生产者和消费者各有若干个 生产者向缓冲区中填数据前需要判断缓冲区是否 ...

  9. linux进程间通信:system V 信号量和共享内存实现进程间同步

    关于信号量和共享内存的相关描述已经在前几篇提到过: 信号量:即内核维护的一个正整数,可以使用内核提供的p/v接口进行该正整数的+/-操作,它主要用来表示系统中可用资源的个数,协调各个进程有序访问资源, ...

最新文章

  1. php去掉字符串的最后一个字符 substr()的用法
  2. 图像处理与计算机视觉:基础,经典以及最近发展(3)计算机视觉中的信号处理与模式识别
  3. 怎样通过css控制table的部分td
  4. 文本分类从入门到精通
  5. python编程题二
  6. linux运维面板_phpstudy linux web面板(小皮面板)V0.2版本正式发布
  7. jdbc增删改查有哪些步骤_用Mybatis如何实现对数据库的增删改查步骤
  8. Bootstrap组件1_字体图标
  9. koa2异常处理_读 koa2 源码后的一些思考与实践
  10. gulp 和npm_为什么我离开Gulp和Grunt去看npm脚本
  11. 开机出现“CPU fan error
  12. 信息学奥赛一本通(2054:【例3.4】适合晨练)
  13. 新iPhone机模曝光:依旧三款配色 难有渐变机身
  14. TokenInsight:BTC新增流量延续上升,链上活跃度保持高位运行
  15. mysql没有makefile_编译安装mysql,找不到makefile
  16. python 日历热力图_Python如何绘制日历图和热力图
  17. 容器技术Docker K8s 1 云原生技术概述
  18. java心得体会2000字_java的学习心得体会
  19. JDK源码阅读调试环境搭建
  20. HttpUtils调用

热门文章

  1. 如何用softmax和sigmoid来做多类分类和多标签分类
  2. 【论文阅读】JDA(joint distribution adaptation)/2013初稿
  3. 带动态属性的自定义标签
  4. python临时笔记
  5. Ubuntu9.04更新源
  6. Linux中的configure、pkg-config、pkg_config_path
  7. 20、在Linux中实现类似windows中获取配置文件的函数GetProfileString
  8. Flash 安全策略配置(1)
  9. 深入Asyncio(八)异步迭代器
  10. Ambari ambari 集群及组件