ConcurrentHashMap 可以做到无锁读,而写使用分段锁机制,把整个哈希表切分成段segment(默认为16段),每段有一个锁,最多可以同时有16个写线程。而读不受限制。

下文转自http://taozeyu.com

ConcurrentHashMap是一个线程安全的哈希实现类,它不但能使多线程同时操作该类时保证线程是安全的,同时为了保证对Map的读操作的高效,完全不使用同步锁。
实现单线程,或简单通过加锁来实现线程安全的一个哈希表所用到的数据结构知识是很普通的,但如果不加锁,也能保证线程安全,则需要用到一些“奇技淫巧”了。
编写JDK的程序员正是这么一些掌握了这种“奇技淫巧”的人。,而ConcurrentHashMap类正是运用了这些技巧的一个实现类。本文将用图片的形式,展现这些技巧。毕竟源代码的阅读是枯燥的。

Java如何实现HashMap

Java规定只有对象才能作为哈希表的Key或Value,这意味着基本数据类型只能存它们的封装对象。一切Java对象都继承自Object,而Object有两个与HashMap紧密相关的方法:
int hashCode()
boolean equals(Object o)

哈希表通过Key对象的hashCode()获取该Key的哈希索引,再通过哈希函数将索引映射到哈希表的某个偏移地址。哈希索引和它经过哈希函数处理后的结果是一个多对一的关系。即同一个哈希索引只可能映射到唯一一个偏移地址上,但同一个偏移地址可能映射到多个哈希索引。

如果多个Key对象被映射到同一个偏移地址(哈希表的设计应该尽量避免这种情况,但有必须对这种情况进行必要的处理) ,称之为冲突。Java解决冲突的方法是拉链法。即多个节点通过链表的形式,占用同一个偏移地址。最终结果可能是这个样子。

当哈希表需要将一个输入的Key对象映射到特定的节点上时,会以如下方式进行映射:

  1. 通过Key对象的hashCode()方法获取哈希索引。
  2. 用哈希函数算出索引对应的偏移地址。ava实现的哈希函数很简单,令哈希索引除以数组长度,取余数即为偏移地址。
    但Java不是简单的取余,它令数组的长度永远是2的整次幂,并以下列式子计算偏移地址。这种算法等价于取余,但效率更高。 
    hashCode & (hashTable.length-1)
  3. 在数组中查看偏移地址信息,如果为空,则没有找到。否则则找到一条链表。
  4. 遍历这条链表,对于链表每一个节点记录的Key对象调用equals方法尝试是否与输入的Key对象相等。
  5. 如果找到,则停止遍历。如果遍历完链表尚未找到,则宣布没有找到。

由此可见,一切存入HashMap中的Key对象,如果想要重写hashCode与equals方法中的任意一个,则必须两个都配套重写。任何企图只重写其中一个方法,或使两个方法不匹配的,都会令程序产生不可预料的结果。

ConcurrentHashMap的实现

但凡单线程的哈希表实现都是很简单的,其知识无外乎《数据结构》中那些要领。但要实现无锁的线程安全的哈希表,则需要一些“巧力”了。让我们看看JDK的程序员是如何做的吧。

ConcurrentHashMap 对于所有的读操作,都不加锁。它仅仅对写操作加锁。这意味着仅仅写操作是互斥的,而读操作则完全不可预测。

首先让我们来看看HashMap中的节点,Entry对象。严格的Entry泛型定义应该是Entry<key,value>,这样就限制了Key和Value的类型。

在 ConcurrentHashMap中,写入的Entry通过无比巧妙的方式,保证了随时可能进行的读操作的安全。我将一一介绍它们的具体实现,并配上图片。

我在配图的时候将用颜色区分不同的节点:

绿色:一切线程都可以看到的对象,因此这些对象必须假定它随时都被访问。

红色:写入线程独占的对象,只有写入线程可见,对于其他任何线程都是透明的。(由于写入线程持有了锁,因此实际上只有一个线程可以访问到这些对象。)

蓝色:即将回收的对象。这些对象可能可见,但是很快随着方法的返回,这些对象将最终变得不可达,而被垃圾回收期搜集处理掉。

(1)put方法

put方法如果发生在Key已存在的情况下,则仅仅是定位Entry,并将它的Value替换成新的。这种情况下完全不需要加锁,且能保证线程安全。因此,我不打算讨论这种情况。我要讨论的是当put发生在Key不存在的情况下的实现。

这种情况下,写入线程首先将找到偏移地址,并遍历整个链表,但发现链表中没有一个Key是可以匹配的,因此线程必须建立一个新的节点。

这种技巧的关键在于,在最后一步完成前,写入线程做造成的影响全部都是“红色”的,即外界不可见的。如果你忽视掉“红色”的方块,则会发现绿色的方块在整个过程中都没有发生任何改变。正是这个特征保证了线程的安全。

(2)remove方法

写入线程首先,找到需要删除的节点。再将需要删除的节点所在链表的前趋全部复制一遍,但直接前趋的Next是指向需要删除节点的后继的。最后,将复制的前趋替换掉之前的前趋。

这种做法等于将待删节点,以及它的前趋的全部前趋都删除掉了(因为变得不可达了),但却为其前趋做了副本,因此真正被删除掉的只有待删节点本身而已。

注意第三部中蓝色的方块,此时如果有线程正在遍历蓝色的方块,对于这条线程而言完全读到任何差异。线程依然是安全的。蓝色的方块只有当所有的线程都不再依赖的时候才会被垃圾回收期搜集。

(3)哈希扩容

哈希表极力降低冲突的概率,因此当哈希表容纳的Entry过多时,会自动扩容。 哈希的扩容后的容量一定为原来的两倍,因此只要哈希表的初始容量是2的整次幂(实际上就是如此),那么哈希表容量一直是2的整次幂。

ConcurrentHashMap使用了一种巧妙的方法,令哈希表即便是在扩容期间,也能保证无锁的读。

第一步将分配新的table,长度为原来的2倍。在遍历原table的每一项,并对链表进行操作(此处将只演示对某一条链表的操作)。首先取出链表最某段的连续的一组节点,此组节点的hashCode在新table中对应的偏移位置(Index)是相同的。

(注:原来一个桶的Entry只可能被重新哈希到两个位置)

再令新table中对应的偏移地址处指向之前选取组的首节点。

将该组节点的前趋全部复制,并各自通过哈希函数计算其在新table中的位置,并插入到该位置上。

这一步仅仅改变指向table的指针,导致原来的旧table以及被复制节点的本体变得不可达了。这些节点最终将被垃圾回收器回收。整个扩容过程即便耗时很长,但对于其他线程都是透明的。

转自:

http://taozeyu.com/software/2014/03/14/concurrent-hash-source-code-analyze.html

ConcurrentHashMap 无锁读的更多相关文章

  1. 线程安全的无锁RingBuffer的实现【一个读线程,一个写线程】

    在程序设计中,我们有时会遇到这样的情况,一个线程将数据写到一个buffer中,另外一个线程从中读数据.所以这里就有多线程竞争的问题.通常的解决办法是对竞争资源加锁.但是,一般加锁的损耗较高.其实,对于 ...

  2. 二、多线程基础-乐观锁_悲观锁_重入锁_读写锁_CAS无锁机制_自旋锁

    1.10乐观锁_悲观锁_重入锁_读写锁_CAS无锁机制_自旋锁1)乐观锁:就像它的名字一样,对于并发间操作产生的线程安全问题持乐观状态,乐观锁认为竞争不总是会发生,因此它不需要持有锁,将 比较-设置 ...

  3. 如何在高并发环境下设计出无锁的数据库操作(Java版本)

    一个在线2k的游戏,每秒钟并发都吓死人.传统的hibernate直接插库基本上是不可行的.我就一步步推导出一个无锁的数据库操作. 1. 并发中如何无锁. 一个很简单的思路,把并发转化成为单线程.Jav ...

  4. 非阻塞同步算法与CAS(Compare and Swap)无锁算法

    锁(lock)的代价 锁是用来做并发最简单的方式,当然其代价也是最高的.内核态的锁的时候需要操作系统进行一次上下文切换,加锁.释放锁会导致比较多的上下文切换和调度延时,等待锁的线程会被挂起直至锁释放. ...

  5. 无锁同步-JAVA之Volatile、Atomic和CAS

    1.概要 本文是无锁同步系列文章的第二篇,主要探讨JAVA中的原子操作,以及如何进行无锁同步. 关于JAVA中的原子操作,我们很容易想到的是Volatile变量.java.util.concurren ...

  6. Java:ConcurrentHashMap的锁分段技术

    术语定义 术语 英文 解释 哈希算法 hash algorithm 是一种将任意内容的输入转换成相同长度输出的加密方式,其输出被称为哈希值.  哈希表 hash table 根据设定的哈希函数H(ke ...

  7. 【Java并发编程】9、非阻塞同步算法与CAS(Compare and Swap)无锁算法

    转自:http://www.cnblogs.com/Mainz/p/3546347.html?utm_source=tuicool&utm_medium=referral 锁(lock)的代价 ...

  8. 线程安全的无锁RingBuffer的实现

    这里的线程安全,是指一个读线程和一个写线程,读写两个线程是安全的,而不是说多个读线程和多个写线程是安全的.. 在程序设计中,我们有时会遇到这样的情况,一个线程将数据写到一个buffer中,另外一个线程 ...

  9. [转]透过 Linux 内核看无锁编程

    非阻塞型同步 (Non-blocking Synchronization) 简介 如何正确有效的保护共享数据是编写并行程序必须面临的一个难题,通常的手段就是同步.同步可分为阻塞型同步(Blocking ...

随机推荐

  1. vue使用vue-cli创建项目

    安装运行环境(node和npm) 安装vue-cli(查看是否安装成功vue -V) 安装webpack 新建项目 1.vue init webpack 项目名称 2.配置项目有关的信息(项目名称,开 ...

  2. CSS 标签显示模式

    标签的类型(显示模式) HTML标签一般分为块标签和行内标签两种类型,它们也称块元素和行内元素. 一.块级元素(block-level) 每个块元素通常都会独自占据一整行或多整行,可以对其设置宽度.高 ...

  3. 自制微擎AI面相识别算术阈值

    有时在朋友圈或其他地方会看到一些AI面相的分享链接或小程序,不是面相算命的有多吸引人,而是前面有"AI"两个字母.于是我就上网找了一下相关代码,发现了一个微擎系统的面相模块.下载下 ...

  4. golang reflect知识集锦

    目录 反射之结构体tag Types vs Kinds reflect.Type vs reflect.Value 2019/4/20 补充 reflect.Value转原始类型 获取类型底层类型 遍 ...

  5. BFS算法的优化 双向宽度优先搜索

    双向宽度优先搜索 (Bidirectional BFS) 算法适用于如下的场景: 无向图 所有边的长度都为 1 或者长度都一样 同时给出了起点和终点 以上 3 个条件都满足的时候,可以使用双向宽度优先 ...

  6. SpringBoot——探究HelloWorld【三】

    前言 前面我们写了helloworld的一个,这里我们对他进行分析 探究 那么下面就开始我们的探究之旅吧,首先从POM文件来,在POM文件中我们导入了项目所需要的依赖 POM文件 父项目 <pa ...

  7. python预课01 turtle学习

    Turtle命令: import turtle # 导入模块 t = turtle.Pen() # 生成画笔 t.speed() #设置速度0-10:0最快 t.forward() # 前进 t.ba ...

  8. 磁盘提示“X:拒绝访问”问题解决

    cacls "D:\*.*" /T /E /G Administrators:F cacls "D:\*.*" /T /E /G Users:F cacls & ...

  9. applyMiddleware 沉思录

    let newStore = applyMiddleware(mid1, mid2, mid3, ...)(createStore)(reducer, null); 给({ getState, dis ...

  10. winform窗体的常用属性