前言

之前在面试的过程中有被问到,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()方法就还是比较简单:

  • 根据keyhashcode寻址到具体的桶上。
  • 如果是红黑树则按照红黑树的方式去查找数据。
  • 如果是链表就按照遍历链表的方式去查找数据。
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方法是线程安全的吗?的更多相关文章

  1. ConcurrentHashMap的size()方法(1.7和1.8)

    在1.7和1.8版本中,计算size()方法有写不同.先介绍1.7版本的实现. 1.7版本 在1.7版本中,有一个重要的类Segment,利用它来实现分段锁 static final class Se ...

  2. ConcurrentHashmap中的size()方法简单解释

    本文所有的源码都是基于JDK1.8 ConcurrentHashmap中的size()方法源码: public int size() { long n = sumCount(); return ((n ...

  3. 并发编程 —— ConcurrentHashMap size 方法原理分析

    前言 ConcurrentHashMap 博大精深,从他的 50 多个内部类就能看出来,似乎 JDK 的并发精髓都在里面了.但他依然拥有体验良好的 API 给我们使用,程序员根本感觉不到他内部的复杂. ...

  4. 使用size()方法输出列表中的元素数量。需要注意的是,这个方法返回的值可能不是真实的,尤其当有线程在添加数据或者移除数据时,这个方法需要遍历整个列表来计算元素数量,而遍历过的数据可能已经改变。仅当没有任何线程修改列表时,才能保证返回的结果是准确的。

    使用size()方法输出列表中的元素数量.需要注意的是,这个方法返回的值可能不是真实的,尤其当有线程在添加数据或者移除数据时,这个方法需要遍历整个列表来计算元素数量,而遍历过的数据可能已经改变.仅当没 ...

  5. Java小知识--length,length(),size()方法详细介绍

    Java中length,length(),size()区别 length属性:用于获取数组长度. eg: int ar[] = new int{1,2,3} /** * 数组用length属性取得长度 ...

  6. 在Java如何保证方法是线程安全的

    废话开篇 都说Java程序好些,但是我觉得Java编程这东西,没个十年八年的真不敢说自己精通Java编程,由于工作原因,开始转战Java多线程,以前没怎么接触过,所以想留点脚印在这两条路上. 切入正题 ...

  7. spring项目中service方法开启线程处理业务的事务问题

    1.前段时间在维护项目的时候碰到一个问题,具体业务就是更新已有角色的资源,数据库已更新,但是权限控制不起效果,还是保留原来的权限. 2.排查发现原有的代码在一个service方法里有进行资源权限表的更 ...

  8. concurrentHashMap求size

    在 JDK1.7 中,首先会使用不加锁的模式去尝试多次计算 ConcurrentHashMap 的 size,最多三次,比较前后计算的结果,结果一致就认为当前没有元素加入,计算的结果是准确的.如果不符 ...

  9. [C#]一个简易的、轻量级的方法并行执行线程辅助类

      一个简易的.轻量级的方法并行执行线程辅助类 在实际应用中,经常要让多个方法并行执行以节约运行时间,线程就是必不可少的了,而多线程的管理经常又是一件头疼的事情,比如方法并行执行异步的返回问题,方法并 ...

随机推荐

  1. 08 vi全屏文本编辑器

    打开文件vim /path/to/somefilevim +# :打开文件,并定位于第#行(如下的 vi +33 profile) vim +:打开文件,定位至最后一行vim +/PATTERN : ...

  2. C#LeetCode刷题之#852-山脉数组的峰顶索引(Peak Index in a Mountain Array)

    问题 该文章的最新版本已迁移至个人博客[比特飞],单击链接 https://www.byteflying.com/archives/4003 访问. 我们把符合下列属性的数组 A 称作山脉: A.le ...

  3. Jdk1.8下的HashMap源码分析

    目录结构 一.面试常见问题 二.基本常量属性 三.构造方法 四.节点结构        4.1 Node类        4.2.TreeNode 五.put方法        5.1 key的has ...

  4. Pandas和常见数据处理小模块

    文章目录 前言 Pandas部分 根据某一列找另一列 根据条件变换每一列 按照标签保存为DataFrame 数据处理 切分数据集和测试集 其他 计时 前言 pandas 确实很好用, 但是网上的教程参 ...

  5. Jira 和 Confluence 企业最佳部署方式

    在Atlassian,我们为客户提供不同的方式来部署 Atlassian 产品:可以部署在由 Altassian 管理的云端(Cloud)上,也可以部署在客户自己选择的服务器(Server)或数据中心 ...

  6. 「MoreThanJava」Day 7:接口详解

    「MoreThanJava」 宣扬的是 「学习,不止 CODE」,本系列 Java 基础教程是自己在结合各方面的知识之后,对 Java 基础的一个总回顾,旨在 「帮助新朋友快速高质量的学习」. 当然 ...

  7. python 常用函数集合

    1.常用函数     round() :  四舍五入         参数1:要处理的小数         参数2:可选,如果不加,就是不要小数,如果加,就是保留几位小数     abs() :绝对值 ...

  8. Kubernetes实战指南(三十三):都0202了,你还在手写k8s的yaml文件?

    目录 1. k8s的yaml文件到底有多复杂 2. 基于图形化的方式自动生成yaml 2.1 k8s图形化管理工具Ratel安装 2.2 使用Ratel创建生成yaml文件 2.2.1 基本配置 2. ...

  9. Typescript node starter 1.Express Typescript

    启动项目 Express 是一个nodejs框架,用于构建Web后端应用程序.它非常的灵活,你可以用你喜欢的方式去使用他.在这个系列文章里,记录了我使用typescript express去构建一个w ...

  10. 用终端命令行(BASH)将本地项目上传到Github并提交代码

    第一步: 在Github上创建自己的repository 第二步:建立本地仓库cd到你的本地项目根目录下,执行git命令 1:$ cd 到你的项目目录下 2:$ git init 第三步:将本地项目工 ...