以下ConcurrentHashMap以jdk8中为例进行分析,ConcurrentHashMap是一个线程安全、基于数组+链表(或者红黑树)的kv容器,主要特性如下:

  • 线程安全,数组中单个slot元素个数超过8个时会将链表结构转换成红黑树,注意树节点之间还是有next指针的;
  • 当元素个数超过N(N = tab.length - tab.length>>>2,达到0.75阈值时)个时触发rehash,成倍扩容;
  • 当线程扩容时,其他线程put数据时会加入帮助扩容,加快扩容速度;
  • put时对单个slot头节点元素进行synchronized加锁,ConcurrentHashMap中的加锁粒度是针对slot节点的,rehash过程中加锁粒度也是如此;
  • get时一般是不加锁。如果slot元素为链表,直接读取返回即可;如果slot元素为红黑树,并且此时该树在进行再平衡或者节点删除操作,读取操作会按照树节点的next指针进行读取,也是不加锁的(因为红黑树中节点也是有链表串起来的);如果该树并没有进行平衡或者节点删除操作,那么会用CAS加读锁,防止读取过程中其他线程该树进行更新操作(主要是防止破坏红黑树节点之间的链表特性),破坏“读视图”。

ConcurrentHashMap默认数组长度16,map最大容量为MAXIMUM_CAPACITY = 1 << 30。创建ConcurrentHashMap并不是涉及数组的初始化,数组初始化是在第一次put数据才进行的。(注意:JDK1.8中舍弃了之前的分段锁技术,改用CAS+Synchronized机制)

Node结构

ConcurrentHashMap中一个重要的类就是Node,该类存储键值对,所有插入ConcurrentHashMap的数据都包装在这里面。它与HashMap中的定义很相似,但是有一些差别是ConcurrentHashMap的value和next属性都是volatile的(保证了get数据时直接返回即可,volatile保证了更新的可见性),且不允许调用setValue方法直接改变Node的value域,增加了find方法辅助map.get()方法,可在get方法返回的结果中更改对应的value值。

 static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
volatile V val;
volatile Node<K,V> next;
}

ConcurrentHashMap定义了三个原子操作,用于对数组指定位置的节点进行操作。正是这些原子操作保证了ConcurrentHashMap的线程安全。

 //获得在i位置上的Node节点
static final <K,V> Node<K,V> tabAt(Node<K,V>[] tab, int i) {
return (Node<K,V>)U.getObjectVolatile(tab, ((long)i << ASHIFT) + ABASE);
}
//利用CAS算法设置i位置上的Node节点。之所以能实现并发是因为他指定了原来这个节点的值是多少
//在CAS算法中,会比较内存中的值与你指定的这个值是否相等,如果相等才接受你的修改,否则拒绝你的修改
//因此当前线程中的值并不是最新的值,这种修改可能会覆盖掉其他线程的修改结果
static final <K,V> boolean casTabAt(Node<K,V>[] tab, int i,
Node<K,V> c, Node<K,V> v) {
return U.compareAndSwapObject(tab, ((long)i << ASHIFT) + ABASE, c, v);
}
//利用volatile方法设置节点位置的值
static final <K,V> void setTabAt(Node<K,V>[] tab, int i, Node<K,V> v) {
U.putObjectVolatile(tab, ((long)i << ASHIFT) + ABASE, v);

下面就按照ConcurrentHashMap的 put / get / remove 来分析下其实现原理,中间涉及rehash、红黑树转换等。

put流程

put操作流程如下:

  • 首先根据key的hashCode计算hash,然后根据hash计算应该在数组中存储位置,如果数据为null,新建数组;
  • 然后通过tabAt(&操作)直接获取对应slot。如果slot为null,则新建kv节点(Node类型)放到slot;
  • 如果当前slot节点的hash值等于MOVED(等于-1),表示其类型为ForwardingNode,证明其他线程在进行rehash扩容操作,当前线程也会帮助一起进行扩容操作;
  • 然后对slot节点进行synchronized加锁,如果slot节点hash值大于等于0,表示当前slot对应元素为链表结构,遍历当前链表,如果key存在则更新,否则添加到链表尾部;如果slot节点类型为TreeBin(其hash值为-2),表示slot对应元素为红黑树,则在红黑树中进行更新节点或者添加节点操作,注意,最后如果树不平衡会进行树的再平衡操作,此时对树root节点加CAS写锁。
  • 最后,如果新添加了节点,会统计map size值;如果当前map数量超过了阈值(N = tab.length - tab.length>>>2)会触发rehash扩容,按照成倍扩容。

注意:因为往map中添加元素和增加元素统计值是两个步骤,不是原子的,所以获取map.size()时可能不是准确值。

对key的hashCode计算hash

存到map中的key并不是直接按照hashCode计算的,因为hashCode有可能为负的,并且不合理的hashCode实现可能导致较多冲突,因此ConcurrentHashMap中会对key对hashCode进行hash操作:

 // int hash = spread(key.hashCode());
// HASH_BITS = 0x7fffffff 符号位设置为0
static final int spread(int h) {
return (h ^ (h >>> 16)) & HASH_BITS;
}

红黑树节点比较

既然使用到了红黑树,这就涉及到节点的大小比较问题(节点数据包含key、value信息)。进行节点的大小比较时,首先是比较节点的hash值,注意hash值不是hashCode,因为hash值是对象hashCode与自己无符号右移16位进行异或后的值。如果节点的hash值相等,判断节点的key对象是否实现了Comparable接口,实现的话就是用Comparable逻辑比较节点之间的大小。如果key对象未实现Comparable接口,则调用tieBreakOrder方法进行判断:

 // dir = tieBreakOrder(k, pk); k/pk,带比较两个节点,命名还是挺有意思的
static int tieBreakOrder(Object a, Object b) {
int d;
if (a == null || b == null ||
(d = a.getClass().getName().
compareTo(b.getClass().getName())) == 0)
d = (System.identityHashCode(a) <= System.identityHashCode(b) ?
-1 : 1);
return d;
}

这里调用了System.identityHashCode,将由默认方法hashCode()返回,如果对象的hashCode()被重写,则System.identityHashCode和hashCode()的返回值就不一样了。

put源码

 final V putVal(K key, V value, boolean onlyIfAbsent) {
// key value非空
if (key == null || value == null) throw new NullPointerException();
int hash = spread(key.hashCode());
// slot对应元素个数,链表转换成红黑树时用
int binCount = 0;
for (Node<K,V>[] tab = table;;) {
Node<K,V> f; int n, i, fh;
if (tab == null || (n = tab.length) == 0)
tab = initTable();
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
if (casTabAt(tab, i, null,
new Node<K,V>(hash, key, value, null)))
break; // no lock when adding to empty bin
}
else if ((fh = f.hash) == MOVED)
// 在rehash扩容,帮助扩容,扩容完成之后才能继续进行put操作
tab = helpTransfer(tab, f);
else {
V oldVal = null;
synchronized (f) { // 加锁
if (tabAt(tab, i) == f) { // 可能已经被更新需要再次进行判断
if (fh >= 0) { // 节点更新或插入
binCount = 1;
for (Node<K,V> e = f;; ++binCount) {
K ek;
if (e.hash == hash &&
((ek = e.key) == key ||
(ek != null && key.equals(ek)))) {
oldVal = e.val;
if (!onlyIfAbsent)
e.val = value;
break;
}
Node<K,V> pred = e;
if ((e = e.next) == null) {
pred.next = new Node<K,V>(hash, key,
value, null);
break;
}
}
}
else if (f instanceof TreeBin) { // 红黑树更新或插入
Node<K,V> p;
binCount = 2;
if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
value)) != null) {
oldVal = p.val;
if (!onlyIfAbsent)
p.val = value;
}
}
}
}
if (binCount != 0) {
if (binCount >= TREEIFY_THRESHOLD)
treeifyBin(tab, i);
if (oldVal != null)
return oldVal;
break;
}
}
}
// 增加统计值,可能触发rehash扩容
addCount(1L, binCount);
return null;
} private final void addCount(long x, int check) {
CounterCell[] as; long b, s;
/**
* counterCells非空表示当前put并发较大,按照counterCells进行分线程统计
* 参考LongAddr思想
*/
if ((as = counterCells) != null ||
!U.compareAndSwapLong(this, BASECOUNT, b = baseCount, s = b + x)) {
CounterCell a; long v; int m;
boolean uncontended = true;
if (as == null || (m = as.length - 1) < 0 ||
(a = as[ThreadLocalRandom.getProbe() & m]) == null ||
!(uncontended =
U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))) {
fullAddCount(x, uncontended);
return;
}
if (check <= 1)
return;
s = sumCount();
}
if (check >= 0) {
Node<K,V>[] tab, nt; int n, sc;
// 大于等于阈值数时进行扩容操作
while (s >= (long)(sc = sizeCtl) && (tab = table) != null &&
(n = tab.length) < MAXIMUM_CAPACITY) {
int rs = resizeStamp(n);
if (sc < 0) {
if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
sc == rs + MAX_RESIZERS || (nt = nextTable) == null ||
transferIndex <= 0)
break;
if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1))
transfer(tab, nt);
}
else if (U.compareAndSwapInt(this, SIZECTL, sc,
(rs << RESIZE_STAMP_SHIFT) + 2))
transfer(tab, null);
s = sumCount();
}
}
}

get流程

get方法比较简单,给定一个key来确定value的时候,必须满足两个条件hash值相同同时 key相同(equals) ,对于节点可能在链表或树上的情况,需要分别去查找。

get时一般是不加锁(Node节点中value数据类型是volatile的,保证了内存可见性)。如果slot元素为链表,直接读取返回即可;如果slot元素为红黑树,并且此时该树在进行再平衡或者节点删除操作,读取操作会按照树节点的next指针进行读取,也是不加锁的;如果该树并没有进行平衡或者节点删除操作,那么会用CAS加读锁,防止读取过程中其他线程该树进行更新操作,破坏“读视图”。

remove流程

remove流程就是根据key找到对应节点,将该节点从链表(更改节点前后关系)或者红黑树移除的过程,注意,从红黑树中删除元素后,不会将红黑树转换为列表的,只能在put元素时列表可能有转换红黑树操作,不会有反向操作。

注意:hashMap有自动rehash扩容机制,但是当元素remove之后并没有自动缩容机制,如果数组经过多次扩容变得很大,并且当前元素较少,请将这些元素转移到一个新的HashMap中。

rehash流程

rehash时是成倍扩容(老table和新tableNew),对于table中i位置的所有元素,扩容后会被分配到i和i+table.length这两个位置中。rehash主要的流程transfer方法中,具体不再展开。

推荐阅读:

更多文档可扫描以下二维码:

别再问我ConcurrentHashMap了的更多相关文章

  1. Java设计模式(十三) 别人再问你设计模式,叫他看这篇文章

    原创文章,转载请务注明出处 OOP三大基本特性 封装 封装,也就是把客观事物封装成抽象的类,并且类可以把自己的属性和方法只让可信的类操作,对不可信的进行信息隐藏. 继承 继承是指这样一种能力,它可以使 ...

  2. 拜托!面试请不要再问我Spring Cloud底层原理[z]

    [z]https://juejin.im/post/5be13b83f265da6116393fc7 拜托!面试请不要再问我Spring Cloud底层原理 欢迎关注微信公众号:石杉的架构笔记(id: ...

  3. vmware 安装配置 ,记住这一次不要再问我了。ok?

    Linux 安装配置 ,记住这一次不要再问我了.ok? 第一步 选择版本 如果遇到问题无法自动获取的  老男孩教育-李泳谊<youjiu_linux@qq.com> 17:51:43明天开 ...

  4. 求你了,再问你Java内存模型的时候别再给我讲堆栈方法区了…

    GitHub 4.1k Star 的Java工程师成神之路 ,不来了解一下吗? GitHub 4.1k Star 的Java工程师成神之路 ,真的不来了解一下吗? GitHub 4.1k Star 的 ...

  5. 面试官,不要再问我“Java GC垃圾回收机制”了

    Java GC垃圾回收几乎是面试必问的JVM问题之一,本篇文章带领大家了解Java GC的底层原理,图文并茂,突破学习及面试瓶颈. 楔子-JVM内存结构补充 在上篇<JVM之内存结构详解> ...

  6. 面试官,不要再问我“Java 垃圾收集器”了

    如果Java虚拟机中标记清除算法.标记整理算法.复制算法.分代算法这些属于GC收集算法中的方法论,那么"GC收集器"则是这些方法论的具体实现. 在面试过程中这个深度的问题涉及的比较 ...

  7. 面试官,不要再问我“Java虚拟机类加载机制”了

    关于Java虚拟机类加载机制往往有两方面的面试题:根据程序判断输出结果和讲讲虚拟机类加载机制的流程.其实这两类题本质上都是考察面试者对Java虚拟机类加载机制的了解. 面试题试水 现在有这样一道判断程 ...

  8. 面试官,不要再问我“Java虚拟机类加载机制”了(转载)

    关于Java虚拟机类加载机制往往有两方面的 面试题:根据程序判断输出结果和讲讲虚拟机类加载机制的流程.其实这两类题本质上都是考察面试者对Java虚拟机类加载机制的了解. 面试题试水 现在有这样一道判断 ...

  9. 面试官,不要再问我“Java 垃圾收集器”了(转载)

    如果Java虚拟机中标记清除算法.标记整理算法.复制算法.分代算法这些属于GC收集算法中的方法论,那么"GC收集器"则是这些方法论的具体实现. 在 面试过程中这个深度的问题涉及的比 ...

随机推荐

  1. Spring Boot-整合redis(六)

    redis安装 参考:https://www.cnblogs.com/LQBlog/p/9214517.html 单机版 1.添加pom依赖 <!-- Spring Boot Reids 依赖 ...

  2. SecureCRT 会话丢失的处理办法

    SecureCRT 会话丢失的处理办法 在SecureCRT中已经有了70多个session,密码都记忆了,当然有些失效的也没有删除: 某一天,打开SecureCRT之后,发现session都没有了, ...

  3. Oracle创建自增字段方法-ORACLE SEQUENCE的简介

    曾经最头疼的就是对表插入数据的时候,有主键问题. 由于主键不可以反复,所以得用函数自己定义一个规则生成不反复的值赋值给主键. 如今发现oracle有sequence就不用那么麻烦了. 转自:http: ...

  4. 自己主动化的在程序中显示SVN版本号

    有时候会有这种情况,策划拿着应用过来提一个bug,但我们却不好确定策划的手机上装的应用相应的是那个代码版本号. 为了解决问题.我们希望能在应用上显示出当前应用所相应的代码版本号,即svn版本号. 构想 ...

  5. 【cocos2d-x 3.7 飞机大战】 决战南海I (八) 背景移动

    採用双层背景.这样效果更好 .h class BackgroundMove : public Layer { public: BackgroundMove(); ~BackgroundMove(); ...

  6. HDU 2604 Queuing,矩阵高速幂

    题目地址:HDU 2604 Queuing 题意:  略 分析: 易推出:   f(n)=f(n-1)+f(n-3)+f(n-4) 构造一个矩阵: 然后直接上板子: /* f[i] = f[i-1] ...

  7. 使用spring mvc或者resteasy构建restful服务

    看到最近一个项目里用resteasy来构建restful接口,有点不明白,不少Spring mvc4.0以后也可以很方面的实现restful服务吗,为啥还要在Spring MVC的项目里还引入rest ...

  8. struts的工作流程

    - 一个请求过来,走前端控制器StrutsPrepareAndExecuteFilter        -前端控制器是一个过滤器,过滤器中的核心方法是doFilter(),doFilter方法中首先处 ...

  9. php pdo具体操作

    0x01:测试PDO是否安装成功 运行如下代码,如果提示参数错误,说明PDO已经安装,如果说明对象不存在,则修改PHP配置文件php.ini,取消php_pdo_yourssqlserverhere. ...

  10. 10分钟教你Python+MySQL数据库操作

    欲直接下载代码文件,关注我们的公众号哦!查看历史消息即可! 本文介绍如何利用python来对MySQL数据库进行操作,本文将主要从以下几个方面展开介绍: 1.数据库介绍 2.MySQL数据库安装和设置 ...