ConcurrentHashMap的size方法是线程安全的吗?
前言
之前在面试的过程中有被问到,ConcurrentHashMap的size方法是线程安全的吗?
这个问题,确实没有答好。这次来根据源码来了解一下,具体是怎么一个实现过程。
ConcurrentHashMap的原理与结构
我们都知道Hash表的结构是数组加链表,就是一个数组中,每一个元素都是一个链表,有时候也把会形象的把数组中的每个元素称为一个“桶”。在插入元素的时候,首先通过对传入的键(key),进行一个哈希函数的处理,来确定元素应该存放于数组中哪个一个元素的链表中。
这种数据结构在很多计算机语言中都能找到其身影,在Java中如HashMap,ConcurrentHashMap等都是这种数据结构。
但是这中数据结构在实现HashMap的时候并不是线程安全的,因为在HashMap扩容的时候,是会将原先的链表迁移至新的链表数组中,在迁移过程中多线程情况下会有造成链表的死循环情况(JDK1.7之前的头插法);还有就是在多线程插入的时候也会造成链表中数据的覆盖导致数据丢失。
所以就出现了线程安全的HashMap类似的hash表集合,典型的就是HashTable和ConcurrentHashMap。
Hashtable实现线程安全的代价比较大,那就是在所有可能产生竞争方法里都加上了synchronized,这样就会导致,当出现竞争的时候只有一个线程能对整个Hashtable进行操作,其他所有线程都需要阻塞等待当前获取到锁的线程执行完成。
这样效率是非常低的。
而ConcurrentHashMap解决线程安全的方式就不一样了,它避免了对整个Map进行加锁,从而提高了并发的效率。
下面将具体介绍一下JDK1.7和1.8的实现。
JDK1.7中的ConcurrentHashMap
JDK1.7中的ConcurrentHashMap采用了分段锁的形式,每一段为一个Segment类,它内部类似HashMap的结构,内部有一个Entry数组,数组的每个元素是一个链表。同时Segment类继承自ReentrantLock
。
结构如下:
在HashEntry中采用了volatile来修饰了HashEntry的当前值和next元素的值。所以get方法在获取数据的时候是不需要加锁的,这样就大大的提供了执行效率。
在执行put()
方法的时候会先尝试获取锁(tryLock()
),如果获取锁失败,说明存在竞争,那么将通过scanAndLockForPut()
方法执行自旋,当自旋次数达到MAX_SCAN_RETRIES
时会执行阻塞锁,直到获取锁成功。
源码如下:
static final int MAX_SCAN_RETRIES =
Runtime.getRuntime().availableProcessors() > 1 ? 64 : 1;
final V put(K key, int hash, V value, boolean onlyIfAbsent) {
// 首先尝试获取锁,获取失败则执行自旋,自旋次数超过最大长度,后改为阻塞锁,直到获取锁成功。
HashEntry<K,V> node = tryLock() ? null :
scanAndLockForPut(key, hash, value);
V oldValue;
try {
HashEntry<K,V>[] tab = table;
int index = (tab.length - 1) & hash;
HashEntry<K,V> first = entryAt(tab, index);
for (HashEntry<K,V> e = first;;) {
if (e != null) {
K k;
if ((k = e.key) == key ||
(e.hash == hash && key.equals(k))) {
oldValue = e.value;
if (!onlyIfAbsent) {
e.value = value;
++modCount;
}
break;
}
e = e.next;
}
else {
if (node != null)
node.setNext(first);
else
node = new HashEntry<K,V>(hash, key, value, first);
int c = count + 1;
if (c > threshold && tab.length < MAXIMUM_CAPACITY)
rehash(node);
else
setEntryAt(tab, index, node);
++modCount;
count = c;
oldValue = null;
break;
}
}
} finally {
unlock();
}
return oldValue;
}
JDK1.8后的ConcurrentHashMap
在JDK1.8中,放弃了Segment这种分段锁的形式,而是采用了CAS+Synchronized
的方式来保证并发操作的,采用了和HashMap一样的结构,直接用数组加链表,在链表长度大于8的时候为了提高查询效率会将链表转为红黑树(链表定位数据的时间复杂度为O(N),红黑树定位数据的时间复杂度为O(logN))。
在代码上也和JDK1.8的HashMap很像,也是将原先的HashEntry改为了Node类,但还是使用了volatile修饰了当前值和next的值。从而保证了在获取数据时候的高效。
JDK1.8中的ConcurrentHashMap在执行put()
方法的时候还是有些复杂的,主要是为了保证线程安全才做了一系列的措施。
源码如下:
- 第一步通过key进行hash。
- 第二步判断是否需要初始化数据结构。
- 第三步根据key定位到当前
Node
,如果当前位置为空,则可以写入数据,利用CAS
机制尝试写入数据,如果写入失败,说明存在竞争,将会通过自旋来保证成功。 - 第四步如果当前的
hashcode
值等于MOVED
则需要进行扩容(扩容时也使用了CAS
来保证了线程安全)。 - 第五步如果上面四步都不满足,那么则通过
synchronized
阻塞锁将数据写入。 - 第六步如果数据量大于
TREEIFY_THRESHOLD
时需要转换成红黑树(默认为8)。
JDK1.8的ConcurrentHashMap的get()
方法就还是比较简单:
- 根据
key
的hashcode
寻址到具体的桶上。 - 如果是红黑树则按照红黑树的方式去查找数据。
- 如果是链表就按照遍历链表的方式去查找数据。
public V get(Object key) {
Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
int h = spread(key.hashCode());
if ((tab = table) != null && (n = tab.length) > 0 &&
(e = tabAt(tab, (n - 1) & h)) != null) {
if ((eh = e.hash) == h) {
if ((ek = e.key) == key || (ek != null && key.equals(ek)))
return e.val;
}
else if (eh < 0)
return (p = e.find(h, key)) != null ? p.val : null;
while ((e = e.next) != null) {
if (e.hash == h &&
((ek = e.key) == key || (ek != null && key.equals(ek))))
return e.val;
}
}
return null;
}
ConcurrentHashMap的size方法
JDK1.7中的ConcurrentHashMap的size方法,计算size的时候会先不加锁获取一次数据长度,然后再获取一次,最多三次。比较前后两次的值,如果相同的话说明不存在竞争的编辑操作,就直接把值返回就可以了。
但是如果前后获取的值不一样,那么会将每个Segment都加上锁,然后计算ConcurrentHashMap的size值。
JDK1.8中的ConcurrentHashMap的size()
方法的源码如下:
/**
* {@inheritDoc}
*/
public int size() {
long n = sumCount();
return ((n < 0L) ? 0 :
(n > (long)Integer.MAX_VALUE) ? Integer.MAX_VALUE :
(int)n);
}
这个方法最大会返回int的最大值,但是ConcurrentHashMap的长度有可能超过int的最大值。
在JDK1.8中增加了mappingCount()
方法,这个方法的返回值是long类型的,所以JDK1.8以后更推荐用这个方法获取Map中数据的数量。
/**
* @return the number of mappings
* @since 1.8
*/
public long mappingCount() {
long n = sumCount();
return (n < 0L) ? 0L : n; // ignore transient negative values
}
无论是size()
方法还是mappingCount()
方法,核心方法都是sumCount()
方法。
源码如下:
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;
}
在上面sumCount()方法中我们看到,当counterCells
为空时直接返回baseCount
,当counterCells
不为空时遍历它并垒加到baseCount
中。
先看baseCount
/**
* Base counter value, used mainly when there is no contention,
* but also as a fallback during table initialization
* races. Updated via CAS.
*/
private transient volatile long baseCount;
baseCount
是一个volatile
变量,那么我们来看在put()
方法执行时是如何使用baseCount的,在put方法的最后一段代码中会调用addCount()
方法,而addCount()
方法的源码如下:
首先对baseCount做CAS自增操作。
如果并发导致了baseCount的CAS失败了,则使用counterCells进行CAS。
如果counterCells的CAS也失败了,那么则进入fullAddCount()
方法,fullAddCount()
方法中会进入死循环,直到成功为止。
那么CountCell到底是个什么呢?
源码如下:
/**
* A padded cell for distributing counts. Adapted from LongAdder
* and Striped64. See their internal docs for explanation.
*/
@sun.misc.Contended static final class CounterCell {
volatile long value;
CounterCell(long x) { value = x; }
}
一个使用了 @sun.misc.Contended
标记的类,内部一个 volatile
变量。
@sun.misc.Contended
这个注解是为了防止“伪共享”。
那么什么是伪共享呢?
缓存系统中是以缓存行(cache line)为单位存储的。缓存行是2的整数幂个连续字节,一般为32-256个字节。最常见的缓存行大小是64个字节。当多线程修改互相独立的变量时,如果这些变量共享同一个缓存行,就会无意中影响彼此的性能,这就是伪共享。
所以伪共享对性能危害极大。
JDK 8 版本之前没有这个注解,JDK1.8之后使用拼接来解决这个问题,把缓存行加满,让缓存之间的修改互不影响。
总结
无论是JDK1.7还是JDK1.8中,ConcurrentHashMap的size()
方法都是线程安全的,都是准确的计算出实际的数量,但是这个数据在并发场景下是随时都在变的。
ConcurrentHashMap的size方法是线程安全的吗?的更多相关文章
- ConcurrentHashMap的size()方法(1.7和1.8)
在1.7和1.8版本中,计算size()方法有写不同.先介绍1.7版本的实现. 1.7版本 在1.7版本中,有一个重要的类Segment,利用它来实现分段锁 static final class Se ...
- ConcurrentHashmap中的size()方法简单解释
本文所有的源码都是基于JDK1.8 ConcurrentHashmap中的size()方法源码: public int size() { long n = sumCount(); return ((n ...
- 并发编程 —— ConcurrentHashMap size 方法原理分析
前言 ConcurrentHashMap 博大精深,从他的 50 多个内部类就能看出来,似乎 JDK 的并发精髓都在里面了.但他依然拥有体验良好的 API 给我们使用,程序员根本感觉不到他内部的复杂. ...
- 使用size()方法输出列表中的元素数量。需要注意的是,这个方法返回的值可能不是真实的,尤其当有线程在添加数据或者移除数据时,这个方法需要遍历整个列表来计算元素数量,而遍历过的数据可能已经改变。仅当没有任何线程修改列表时,才能保证返回的结果是准确的。
使用size()方法输出列表中的元素数量.需要注意的是,这个方法返回的值可能不是真实的,尤其当有线程在添加数据或者移除数据时,这个方法需要遍历整个列表来计算元素数量,而遍历过的数据可能已经改变.仅当没 ...
- Java小知识--length,length(),size()方法详细介绍
Java中length,length(),size()区别 length属性:用于获取数组长度. eg: int ar[] = new int{1,2,3} /** * 数组用length属性取得长度 ...
- 在Java如何保证方法是线程安全的
废话开篇 都说Java程序好些,但是我觉得Java编程这东西,没个十年八年的真不敢说自己精通Java编程,由于工作原因,开始转战Java多线程,以前没怎么接触过,所以想留点脚印在这两条路上. 切入正题 ...
- spring项目中service方法开启线程处理业务的事务问题
1.前段时间在维护项目的时候碰到一个问题,具体业务就是更新已有角色的资源,数据库已更新,但是权限控制不起效果,还是保留原来的权限. 2.排查发现原有的代码在一个service方法里有进行资源权限表的更 ...
- concurrentHashMap求size
在 JDK1.7 中,首先会使用不加锁的模式去尝试多次计算 ConcurrentHashMap 的 size,最多三次,比较前后计算的结果,结果一致就认为当前没有元素加入,计算的结果是准确的.如果不符 ...
- [C#]一个简易的、轻量级的方法并行执行线程辅助类
一个简易的.轻量级的方法并行执行线程辅助类 在实际应用中,经常要让多个方法并行执行以节约运行时间,线程就是必不可少的了,而多线程的管理经常又是一件头疼的事情,比如方法并行执行异步的返回问题,方法并 ...
随机推荐
- python5.1文件的读取
fh1=open(r"C:\222.txt","r")#用open函数读取文件,“r”进行转义,fh1文件句柄data=fh1.read()#把读取的句柄赋值给 ...
- @RestController注解初步理解
一.在Spring中@RestController的作用等同于@Controller + @ResponseBody. 所以想要理解@RestController注解就要先了解@Controller和 ...
- JS 动画笔记
动画实现原理 核心原理:通过定时器setInterval()不断地移动盒子的位置 缓动动画原理 缓动的核心算法~~~~~~~~~~(目标值-现在的位置)/10 停止的条件是~~ ...
- Phthon环境搭建
一,官网去下载 https://www.python.org/downloads/ 二,安装 三,验证python 四.IPython IPython 是一个 python 的交互式 shell,比默 ...
- ES6语法学习(一)-let和const
1.let 和 const 变量提升: 在声明变量或者函数时,被声明的变量和函数会被提升到函数最顶部: 但是如果声明的变量或者函数被初始化了,则会失去变量提升: 示例代码: param2 = &quo ...
- MQC集群配置错误
这个错误说明配置里面,MQC环境配置错了 运行war包时会读到本地的配置system-config.propertites文件
- GitHub 热点速览 Vol.33:听说程序员都是颜控?
作者:HelloGitHub-小鱼干 摘要:GitHub上面除了很多有趣的项目,也有很多高颜值的项目,比如:图片共享网站 Unsplash 开源的 Dataset,提供了两百多张高颜值美照.Heroi ...
- Spark QuantileDiscretizer 分位数离散器
1.概念 接收具有连续特征的列,并输出具有合并分类特征的列.按分位数,对给出的数据列进行离散化分箱处理. 和Bucketizer(分箱处理)一样也是:将连续数值特征转换为离散类别特征.实际上Class ...
- 操作系统-中断(2)IA-32/Linux的向量中断方式
一.Intel定义下的异常和中断 不同体系和教材往往对异常和中断有不同的定义. Intel定义:中断是一种典型的由I/O设备触发的.与当前正在执行的指令无关的异步事件:而异常是处理器执行一条指令时,由 ...
- 牛客网PAT练习场-有几个PAT
题目地址:https://www.nowcoder.com/pat/6/problem/4066 题意:求pat->求pa->求p /** * *作者:YCute *时间:2019-12- ...