谈谈ConcurrentHashMap1.7和1.8的不同实现
知止而后有定,定而后能静,静而后能安,安而后能虑,虑而后能得。
ConcurrentHashMap
在多线程环境下,使用HashMap
进行put
操作时存在丢失数据的情况,为了避免这种bug的隐患,强烈建议使用ConcurrentHashMap
代替HashMap
,为了对ConcurrentHashMap
有更深入的了解,本文将对ConcurrentHashMap
1.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的不同实现的更多相关文章
- Java面试02|Java集合
关于Java中并发集合有: (1)CouncurrentHashMap (2)CopyOnWriteArrayList (3)LinkedBlockingQueue (4)ArrayBlockingQ ...
- ConcurrentHashmap中的size()方法简单解释
本文所有的源码都是基于JDK1.8 ConcurrentHashmap中的size()方法源码: public int size() { long n = sumCount(); return ((n ...
- 为并发而生的 ConcurrentHashMap(Java 8)
HashMap 是我们日常最常见的一种容器,它以键值对的形式完成对数据的存储,但众所周知,它在高并发的情境下是不安全的.尤其是在 jdk 1.8 之前,rehash 的过程中采用头插法转移结点,高并发 ...
- 在Java路上,我看过的一些书、源码和框架(转)
原文地址:http://www.jianshu.com/p/4a41ee88bd82 物有本末,事有终始,知所先后,则近道矣 面试经历 关于Java面试,你应该准备这些知识点关于Java面试,你应该准 ...
- Java 容器源码分析之ConcurrentHashMap
深入浅出ConcurrentHashMap(1.8) 前言 HashMap是我们平时开发过程中用的比较多的集合,但它是非线程安全的,在涉及到多线程并发的情况,进行put操作有可能会引起死循环,导致CP ...
- ConcurrentHashMap原理分析(1.7与1.8)-put和 get 需要执行两次Hash
ConcurrentHashMap 与HashMap和Hashtable 最大的不同在于:put和 get 两次Hash到达指定的HashEntry,第一次hash到达Segment,第二次到达Seg ...
- Java并发包源码学习系列:JDK1.8的ConcurrentHashMap源码解析
目录 为什么要使用ConcurrentHashMap? ConcurrentHashMap的结构特点 Java8之前 Java8之后 基本常量 重要成员变量 构造方法 tableSizeFor put ...
- 【原】谈谈对Objective-C中代理模式的误解
[原]谈谈对Objective-C中代理模式的误解 本文转载请注明出处 —— polobymulberry-博客园 1. 前言 这篇文章主要是对代理模式和委托模式进行了对比,个人认为Objective ...
- 谈谈一些有趣的CSS题目(十二)-- 你该知道的字体 font-family
开本系列,谈谈一些有趣的 CSS 题目,题目类型天马行空,想到什么说什么,不仅为了拓宽一下解决问题的思路,更涉及一些容易忽视的 CSS 细节. 解题不考虑兼容性,题目天马行空,想到什么说什么,如果解题 ...
随机推荐
- 怎样用Javascript定义一个类
其实Javascript中没有类这个定义,但是有类这个概念.很多人都写过这样的代码,对,没错,就是如下代码,清晰的不能再清晰了,就是一个关键字 function,然后定义一个方法名,方法名后紧跟一对括 ...
- 【View】之【SimpleWaveView】可多色可刷新的加速球、进度球【demo】
当前版本:SimpleWaveView_v1.0.20140618 先看效果图,这个加速球是动态的,并且当调用了myView.setRefresh(0.8F);方法后可以从当前值动态降到0再升到80% ...
- mysql中,root用户密码被遗忘,该如何进行重置?
需求描述: 在mysql的测试环境中,有时候会遇到一段时间之后root用户的密码被遗忘的情况, 这个时候,就是需要对root密码进行重置,不过,在生产环境中,这种情况还是很少见. 环境描述: 操作系统 ...
- Tomcat 8(九)解读Tomcat组件的生命周期(Lifecycle)
Tomcat 8(七)解读Bootstrap介绍过.运行startup.bat.将引发Tomcat一连串组件的启动.事实上这一连串启动是通过组件的生命周期(Lifecycle)实现的 今天来看看Lif ...
- HTTP 基础术语
URI 和 URL:URI用于标记一个网络资源,URL则表示这个网络资源的访问地址,详细说明 超文本:普通的一段文字叫做文本,如果给这段文字加上超链接,那么就叫做超文本,HTML 就是超文本标记语言 ...
- 通过Nagios监控weblogic服务
1.前言 前段时间搭建了一套Nagios监控服务,心血来潮想自己写一个脚本,拓展Nagios插件来监控公司的weblogic服务. 环境:weblogic10.3.3.0 . CentOS6 ...
- 浅谈Socket长连+多线程
缘由 不知各位同仁有没有发现,用简单,无外乎就是都是一个流程 1)监听链接 2)校验链接是否是正常链接 3)保存链接至全局静态字典 4)开启新线程监听监听到的线程报文 5)执行对应命令或者发送对应命令 ...
- 使用Node.js完成路由
首先先看一下文件的结构: 我想通过改变不同的路由进不同的页面, 先看这几个HTML页面: 404: <!DOCTYPE html> <html lang="en" ...
- option 选不中问题
function appAndBuz(appName,buzName,areaCenterCode){ //appName,buzName下拉框的值start $.ajax({ type: " ...
- setTimeout原来有这种用途
setTimeout有两个参数,第一个是需要执行的函数,第二个是将该函数推入UI队列的时间. 需要注意的两点: 1.第二个参数中设置的时间,是从执行setTimeout开始计算,而不是从整个函数执行完 ...