ConcurrentHashMap1.7和1.8的不同实现
ConcurrentHashMap
在多线程环境下,使用HashMap进行put操作时存在丢失数据的情况,为了避免这种bug的隐患,强烈建议使用ConcurrentHashMap代替HashMap,为了对ConcurrentHashMap有更深入的了解,本文将对ConcurrentHashMap1.7和1.8的不同实现进行分析。
1.7实现
数据结构
jdk1.7中采用Segment + HashEntry的方式进行实现,结构如下:

ConcurrentHashMap初始化时,计算出Segment数组的大小ssize和每个Segment中HashEntry数组的大小cap,并初始化Segment数组的第一个元素;其中ssize大小为2的幂次方,默认为16,cap大小也是2的幂次方,最小值为2,最终结果根据根据初始化容量initialCapacity进行计算,计算过程如下:
|
1
2
3
4
5
|
if (c * ssize < initialCapacity) ++c;int cap = MIN_SEGMENT_TABLE_CAPACITY;while (cap < c) cap <<= 1; |
其中Segment在实现上继承了ReentrantLock,这样就自带了锁的功能。
put实现
当执行put方法插入数据时,根据key的hash值,在Segment数组中找到相应的位置,如果相应位置的Segment还未初始化,则通过CAS进行赋值,接着执行Segment对象的put方法通过加锁机制插入数据,实现如下:
场景:线程A和线程B同时执行相同Segment对象的put方法
1、线程A执行tryLock()方法成功获取锁,则把HashEntry对象插入到相应的位置;
2、线程B获取锁失败,则执行scanAndLockForPut()方法,在scanAndLockForPut方法中,会通过重复执行tryLock()方法尝试获取锁,在多处理器环境下,重复次数为64,单处理器重复次数为1,当执行tryLock()方法的次数超过上限时,则执行lock()方法挂起线程B;
3、当线程A执行完插入操作时,会通过unlock()方法释放锁,接着唤醒线程B继续执行;
size实现
因为ConcurrentHashMap是可以并发插入数据的,所以在准确计算元素时存在一定的难度,一般的思路是统计每个Segment对象中的元素个数,然后进行累加,但是这种方式计算出来的结果并不一样的准确的,因为在计算后面几个Segment的元素个数时,已经计算过的Segment同时可能有数据的插入或则删除,在1.7的实现中,采用了如下方式:
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
|
try { for (;;) { if (retries++ == RETRIES_BEFORE_LOCK) { for (int j = 0; j < segments.length; ++j) ensureSegment(j).lock(); // force creation } sum = 0L; size = 0; overflow = false; for (int j = 0; j < segments.length; ++j) { Segment<K,V> seg = segmentAt(segments, j); if (seg != null) { sum += seg.modCount; int c = seg.count; if (c < 0 || (size += c) < 0) overflow = true; } } if (sum == last) break; last = sum; }} finally { if (retries > RETRIES_BEFORE_LOCK) { for (int j = 0; j < segments.length; ++j) segmentAt(segments, j).unlock(); }} |
先采用不加锁的方式,连续计算元素的个数,最多计算3次:
1、如果前后两次计算结果相同,则说明计算出来的元素个数是准确的;
2、如果前后两次计算结果都不同,则给每个Segment进行加锁,再计算一次元素的个数;
1.8实现
数据结构
1.8中放弃了Segment臃肿的设计,取而代之的是采用Node + CAS + Synchronized来保证并发安全进行实现,结构如下:

只有在执行第一次put方法时才会调用initTable()初始化Node数组,实现如下:
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
private final Node<K,V>[] initTable() { Node<K,V>[] tab; int sc; while ((tab = table) == null || tab.length == 0) { if ((sc = sizeCtl) < 0) Thread.yield(); // lost initialization race; just spin else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) { try { if ((tab = table) == null || tab.length == 0) { int n = (sc > 0) ? sc : DEFAULT_CAPACITY; @SuppressWarnings("unchecked") Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n]; table = tab = nt; sc = n - (n >>> 2); } } finally { sizeCtl = sc; } break; } } return tab;} |
put实现
当执行put方法插入数据时,根据key的hash值,在Node数组中找到相应的位置,实现如下:
1、如果相应位置的Node还未初始化,则通过CAS插入相应的数据;
|
1
2
3
4
|
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} |
2、如果相应位置的Node不为空,且当前该节点不处于移动状态,则对该节点加synchronized锁,如果该节点的hash不小于0,则遍历链表更新节点或插入新节点;
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
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; } }} |
3、如果该节点是TreeBin类型的节点,说明是红黑树结构,则通过putTreeVal方法往红黑树中插入节点;
|
1
2
3
4
5
6
7
8
9
|
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; }} |
4、如果binCount不为0,说明put操作对数据产生了影响,如果当前链表的个数达到8个,则通过treeifyBin方法转化为红黑树,如果oldVal不为空,说明是一次更新操作,没有对元素个数产生影响,则直接返回旧值;
|
1
2
3
4
5
6
7
|
if (binCount != 0) { if (binCount >= TREEIFY_THRESHOLD) treeifyBin(tab, i); if (oldVal != null) return oldVal; break;} |
5、如果插入的是一个新节点,则执行addCount()方法尝试更新元素个数baseCount;
size实现
1.8中使用一个volatile类型的变量baseCount记录元素的个数,当插入新数据或则删除数据时,会通过addCount()方法更新baseCount,实现如下:
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
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();} |
1、初始化时counterCells为空,在并发量很高时,如果存在两个线程同时执行CAS修改baseCount值,则失败的线程会继续执行方法体中的逻辑,使用CounterCell记录元素个数的变化;
2、如果CounterCell数组counterCells为空,调用fullAddCount()方法进行初始化,并插入对应的记录数,通过CAS设置cellsBusy字段,只有设置成功的线程才能初始化CounterCell数组,实现如下:
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
else if (cellsBusy == 0 && counterCells == as && U.compareAndSwapInt(this, CELLSBUSY, 0, 1)) { boolean init = false; try { // Initialize table if (counterCells == as) { CounterCell[] rs = new CounterCell[2]; rs[h & 1] = new CounterCell(x); counterCells = rs; init = true; } } finally { cellsBusy = 0; } if (init) break;} |
3、如果通过CAS设置cellsBusy字段失败的话,则继续尝试通过CAS修改baseCount字段,如果修改baseCount字段成功的话,就退出循环,否则继续循环插入CounterCell对象;
|
1
2
|
else if (U.compareAndSwapLong(this, BASECOUNT, v = baseCount, v + x)) break; |
所以在1.8中的size实现比1.7简单多,因为元素个数保存baseCount中,部分元素的变化个数保存在CounterCell数组中,实现如下:
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
public int size() { long n = sumCount(); return ((n < 0L) ? 0 : (n > (long)Integer.MAX_VALUE) ? Integer.MAX_VALUE : (int)n);}final long sumCount() { CounterCell[] as = counterCells; CounterCell a; long sum = baseCount; if (as != null) { for (int i = 0; i < as.length; ++i) { if ((a = as[i]) != null) sum += a.value; } } return sum;} |
通过累加baseCount和CounterCell数组中的数量,即可得到元素的总个数;
ConcurrentHashMap1.7和1.8的不同实现的更多相关文章
- 深入浅出ConcurrentHashMap1.8
转载:https://www.jianshu.com/p/c0642afe03e0 好文 关于文章中的疑问:为什么要构造一个反序链表,放在nextTable的i+n的位置上呢,在<深入分析Con ...
- 深入浅出ConcurrentHashMap1.8+CAS+volatile
1.深入浅出CAS 前言 CAS(Compare and Swap),即比较并替换,实现并发算法时常用到的一种技术,Doug lea大神在java同步器中大量使用了CAS技术,鬼斧神工的实现了多线程执 ...
- 学习ConcurrentHashMap1.7分段锁原理
1. 概述 接上一篇 学习 ConcurrentHashMap1.8 并发写机制, 本文主要学习 Segment分段锁 的实现原理. 虽然 JDK1.7 在生产环境已逐渐被 JDK1.8 替代,然而一 ...
- ConcurrentHashMap1.8源码分析
文章简介 想必大家对HashMap数据结构并不陌生,JDK1.7采用的是数组+链表的方式,JDK1.8采用的是数组+链表+红黑树的方式.虽然JDK1.8对于HashMap有了很大的改进,提高了存取效率 ...
- ConcurrentHashMap1.8源码解析
深入并发包 ConcurrentHashMap 概述 JDK1.8的实现已经摒弃了Segment的概念,而是直接用Node数组+链表+红黑树的数据结构来实现,并发控制使用Synchronized和CA ...
- 谈谈ConcurrentHashMap1.7和1.8的不同实现
知止而后有定,定而后能静,静而后能安,安而后能虑,虑而后能得. ConcurrentHashMap 在多线程环境下,使用HashMap进行put操作时存在丢失数据的情况,为了避免这种bug的隐患,强烈 ...
- 理解ConcurrentHashMap1.8源码
ConcurrentHashMap源码分析 其实ConcurrentHashMap我自己已经看过很多遍了,但是今天在面试阿里的时候自己在描述ConcurrentHashMap发现自己根本讲不清楚什么是 ...
- ConcurrentHashMap1.7源码分析
参考:https://www.cnblogs.com/liuyun1995/p/8631264.html HashMap不是线程安全的,其所有的方法都未同步,虽然可以使用Collections的syn ...
- ConcurrentHashMap1.7和1.8的源码分析比较
ConcurrentHashMap 在多线程环境下,使用HashMap进行put操作时存在丢失数据的情况,为了避免这种bug的隐患,强烈建议使用ConcurrentHashMap代替HashMap,为 ...
随机推荐
- Linux实战案例(2)实例讲解使用软连接的场景和过程
=================================== 使用场景:使用软连接简化版本切换动作 进入操作目录, cd /opt/modules/ ==================== ...
- Java设计模式(八)Proxy代理模式
一.场景描述 代理在生活中并不少见,租房子需要找中介,打官司需要找律师,很多事情我们需要找专业人士代理我们做,另一方面,中介和律师也代理了房东.法律程序与我们打交道. 当然,设计模式中的代理与广义的代 ...
- mysql 索引学习--多条件等值查询,顺序不同也能应用联合索引啦
以前学习这一块的时候,是说:假设建立了联合索引a+b,那么查询语句也一定要是这个顺序才能应用该索引. 那么实际是怎样呢,经过mysql这么多次版本升级,相信mysql已经给我们做了某些优化. 下面是我 ...
- ehcache.xml 属性大全
属性大全 name:缓存名称. maxElementsInMemory:缓存最大个数. eternal:对象是否永久有效,一但设置了,timeout将不起作用. timeToIdleSeconds:设 ...
- SpringBoot(五):@ConfigurationProperties配置参数绑定
在springmvc或其他ssh框架中如果我们要实现一个配置参数的加载,需要使用代码实现读取properties文件等操作,或者需要使用其他属性@value(name="username&q ...
- c语言文件中关于while(!feof(fp)) 循环多输出一次的问题
文件中关于while(!feof(fp)) 循环多输出一次的问题 feof(fp)有两个返回值:如果遇到文件结束,函数feof(fp)的值为1,否则为0. 当读到文件末尾时,文件指针并没有 ...
- 尼姆博弈(Nimm's Game)
题型: 有3堆各若干个物品,两个人轮流从某一堆取任意多的物品,规定每次至少取1个,多者不限,最后取光者得胜. 思路 首先自己想一下,就会发现只要最后剩两堆物品一样多(不为零),第三堆为零,那面对这种局 ...
- Linux OpenGL 实践篇-5 纹理
纹理 在之前的实践中,我们所渲染的物体的表面颜色都是纯色或者根据顶点位置计算出的一个颜色,这种方式在表现物体细节方面是比较吃资源的,因为我们每增加一个细节,我们就需要定义更多的顶点及其属性.所以美术人 ...
- LeetCode Binary Search Summary 二分搜索法小结
二分查找法作为一种常见的查找方法,将原本是线性时间提升到了对数时间范围,大大缩短了搜索时间,具有很大的应用场景,而在LeetCode中,要运用二分搜索法来解的题目也有很多,但是实际上二分查找法的查找目 ...
- mybatis学习一
1:ORM概念 ORM(OBJECT-RELATIONSHIP MAPPING) 即对象关系映射,是一种思想,实质是将数据库中的数据用对象的形式表现出来 JPA(JAVA PERSISIT ...