今天我们介绍一下ConcurrentHashMap在JDK1.8中的实现。
基本结构

ConcurrentHashMap在1.8中的实现,相比于1.7的版本基本上全部都变掉了。首先,取消了Segment分段锁的数据结构,取而代之的是数组+链表(红黑树)的结构。而对于锁的粒度,调整为对每个数组元素加锁(Node)。然后是定位节点的hash算法被简化了,这样带来的弊端是Hash冲突会加剧。因此在链表节点数量大于8时,会将链表转化为红黑树进行存储。这样一来,查询的时间复杂度就会由原先的O(n)变为O(logN)。下面是其基本结构:

相关属性

  1. private transient volatile int sizeCtl;

sizeCtl用于table[]的初始化和扩容操作,不同值的代表状态如下:

  • -1:table[]正在初始化。
  • -N:表示有N-1个线程正在进行扩容操作。

非负情况:

  1. 如果table[]未初始化,则表示table需要初始化的大小。
  2. 如果初始化完成,则表示table[]扩容的阀值,默认是table[]容量的0.75 倍。
  1. private static finalint DEFAULT_CONCURRENCY_LEVEL = 16;
  • DEFAULT_CONCURRENCY_LEVEL:表示默认的并发级别,也就是table[]的默认大小。
  1. private static final float LOAD_FACTOR = 0.75f;
  • LOAD_FACTOR:默认的负载因子。
  1. static final int TREEIFY_THRESHOLD = 8;
  • TREEIFY_THRESHOLD:链表转红黑树的阀值,当table[i]下面的链表长度大于8时就转化为红黑树结构。
  1. static final int UNTREEIFY_THRESHOLD = 6;
  • UNTREEIFY_THRESHOLD:红黑树转链表的阀值,当链表长度<=6时转为链表(扩容时)。

构造函数

  1. public ConcurrentHashMap(int initialCapacity,
  2. float loadFactor, int concurrencyLevel) {
  3. if (!(loadFactor > 0.0f) || initialCapacity < 0 || concurrencyLevel <= 0)
  4. throw new IllegalArgumentException();
  5. if (initialCapacity < concurrencyLevel)   // 初始化容量至少要为concurrencyLevel
  6. initialCapacity = concurrencyLevel;
  7. long size = (long)(1.0 + (long)initialCapacity / loadFactor);
  8. int cap = (size >= (long)MAXIMUM_CAPACITY) ?
  9. MAXIMUM_CAPACITY : tableSizeFor((int)size);
  10. this.sizeCtl = cap;
  11. }

从上面代码可以看出,在创建ConcurrentHashMap时,并没有初始化table[]数组,只对Map容量,并发级别等做了赋值操作。
相关节点

  1. Node:该类用于构造table[],只读节点(不提供修改方法)。
  2. TreeBin:红黑树结构。
  3. TreeNode:红黑树节点。
  4. ForwardingNode:临时节点(扩容时使用)。

put()操作

  1. public V put(K key, V value) {
  2. return putVal(key, value, false);
  3. }
  4. final V putVal(K key, V value, boolean onlyIfAbsent) {
  5. if (key == null || value == null) throw new NullPointerException();
  6. int hash = spread(key.hashCode());
  7. int binCount = 0;
  8. for (Node<K,V>[] tab = table;;) {
  9. Node<K,V> f; int n, i, fh;
  10. if (tab == null || (n = tab.length) == 0)// 若table[]未创建,则初始化
  11. tab = initTable();
  12. else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {// table[i]后面无节点时,直接创建Node(无锁操作)
  13. if (casTabAt(tab, i, null,
  14. new Node<K,V>(hash, key, value, null)))
  15. break;                   // no lock when adding to empty bin
  16. }
  17. else if ((fh = f.hash) == MOVED)// 如果当前正在扩容,则帮助扩容并返回最新table[]
  18. tab = helpTransfer(tab, f);
  19. else {// 在链表或者红黑树中追加节点
  20. V oldVal = null;
  21. synchronized (f) {// 这里并没有使用ReentrantLock,说明synchronized已经足够优化了
  22. if (tabAt(tab, i) == f) {
  23. if (fh >= 0) {// 如果为链表结构
  24. binCount = 1;
  25. for (Node<K,V> e = f;; ++binCount) {
  26. K ek;
  27. if (e.hash == hash &&
  28. ((ek = e.key) == key ||
  29. (ek != null && key.equals(ek)))) {// 找到key,替换value
  30. oldVal = e.val;
  31. if (!onlyIfAbsent)
  32. e.val = value;
  33. break;
  34. }
  35. Node<K,V> pred = e;
  36. if ((e = e.next) == null) {// 在尾部插入Node
  37. pred.next = new Node<K,V>(hash, key,
  38. value, null);
  39. break;
  40. }
  41. }
  42. }
  43. else if (f instanceof TreeBin) {// 如果为红黑树
  44. Node<K,V> p;
  45. binCount = 2;
  46. if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
  47. value)) != null) {
  48. oldVal = p.val;
  49. if (!onlyIfAbsent)
  50. p.val = value;
  51. }
  52. }
  53. }
  54. }
  55. if (binCount != 0) {
  56. if (binCount >= TREEIFY_THRESHOLD)// 到达阀值,变为红黑树结构
  57. treeifyBin(tab, i);
  58. if (oldVal != null)
  59. return oldVal;
  60. break;
  61. }
  62. }
  63. }
  64. addCount(1L, binCount);
  65. return null;
  66. }

从上面代码可以看出,put的步骤大致如下:

  1. 参数校验。
  2. 若table[]未创建,则初始化。
  3. 当table[i]后面无节点时,直接创建Node(无锁操作)。
  4. 如果当前正在扩容,则帮助扩容并返回最新table[]。
  5. 然后在链表或者红黑树中追加节点。
  6. 最后还回去判断是否到达阀值,如到达变为红黑树结构。

除了上述步骤以外,还有一点我们留意到的是,代码中加锁片段用的是synchronized关键字,而不是像1.7中的ReentrantLock。这一点也说明了,synchronized在新版本的JDK中优化的程度和ReentrantLock差不多了。
get()操作

  1. public V get(Object key) {
  2. Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
  3. int h = spread(key.hashCode());// 定位到table[]中的i
  4. if ((tab = table) != null && (n = tab.length) > 0 &&
  5. (e = tabAt(tab, (n - 1) & h)) != null) {// 若table[i]存在
  6. if ((eh = e.hash) == h) {// 比较链表头部
  7. if ((ek = e.key) == key || (ek != null && key.equals(ek)))
  8. return e.val;
  9. }
  10. else if (eh < 0)// 若为红黑树,查找树
  11. return (p = e.find(h, key)) != null ? p.val : null;
  12. while ((e = e.next) != null) {// 循环链表查找
  13. if (e.hash == h &&
  14. ((ek = e.key) == key || (ek != null && key.equals(ek))))
  15. return e.val;
  16. }
  17. }
  18. return null;// 未找到
  19. }

get()方法的流程相对简单一点,从上面代码可以看出以下步骤:

  1. 首先定位到table[]中的i。
  2. 若table[i]存在,则继续查找。
  3. 首先比较链表头部,如果是则返回。
  4. 然后如果为红黑树,查找树。
  5. 最后再循环链表查找。

从上面步骤可以看出,ConcurrentHashMap的get操作上面并没有加锁。所以在多线程操作的过程中,并不能完全的保证一致性。这里和1.7当中类似,是弱一致性的体现。
size()操作

  1. // 1.2时加入
  2. public int size() {
  3. long n = sumCount();
  4. return ((n < 0L) ? 0 :
  5. (n > (long)Integer.MAX_VALUE) ? Integer.MAX_VALUE :
  6. (int)n);
  7. }
  8. // 1.8加入的API
  9. public long mappingCount() {
  10. long n = sumCount();
  11. return (n < 0L) ? 0L : n; // ignore transient negative values
  12. }
  13. final long sumCount() {
  14. CounterCell[] as = counterCells; CounterCell a;
  15. long sum = baseCount;
  16. if (as != null) {
  17. for (int i = 0; i < as.length; ++i) {
  18. if ((a = as[i]) != null)
  19. sum += a.value;
  20. }
  21. }
  22. return sum;
  23. }

从上面代码可以看出来,JDK1.8中新增了一个mappingCount()的API。这个API与size()不同的就是返回值是Long类型,这样就不受Integer.MAX_VALUE的大小限制了。
        两个方法都同时调用了,sumCount()方法。对于每个table[i]都有一个CounterCell与之对应,上面方法做了求和之后就返回了。从而可以看出,size()和mappingCount()返回的都是一个估计值(这一点与JDK1.7里面的实现不同,1.7里面使用了加锁的方式实现。这里面也可以看出JDK1.8牺牲了精度,来换取更高的效率。)

ConcurrentHashMap的JDK1.8实现的更多相关文章

  1. Java并发编程总结4——ConcurrentHashMap在jdk1.8中的改进(转)

    一.简单回顾ConcurrentHashMap在jdk1.7中的设计 先简单看下ConcurrentHashMap类在jdk1.7中的设计,其基本结构如图所示: 每一个segment都是一个HashE ...

  2. Java并发编程总结4——ConcurrentHashMap在jdk1.8中的改进

    一.简单回顾ConcurrentHashMap在jdk1.7中的设计 先简单看下ConcurrentHashMap类在jdk1.7中的设计,其基本结构如图所示: 每一个segment都是一个HashE ...

  3. ConcurrentHashMap基于JDK1.8源码剖析

    前言 声明,本文用的是jdk1.8 前面章节回顾: Collection总览 List集合就这么简单[源码剖析] Map集合.散列表.红黑树介绍 HashMap就是这么简单[源码剖析] LinkedH ...

  4. 源码分析(4)-ConcurrentHashMap(JDK1.8)

    一.UML类图 ConcurrentHashMap键值不能为null:底层数据结构是数组+链表/红黑二叉树:采用CAS(比较并交换)和synchronized来保证并发安全. CAS文章:https: ...

  5. ConcurrentHashMap (jdk1.7)源码学习

    一.介绍 1.Segment(分段锁) 1.1 Segment 容器里有多把锁,每一把锁用于锁容器其中一部分数据,那么当多线程访问容器里不同数据段的数据时,线程间就不会存在锁竞争,从而可以有效的提高并 ...

  6. 多线程之并发容器ConcurrentHashMap(JDK1.6)

    简介 ConcurrentHashMap 是 util.concurrent 包的重要成员.本文将结合 Java 内存模型,分析 JDK 源代码,探索 ConcurrentHashMap 高并发的具体 ...

  7. java 并发容器一之ConcurrentHashMap(基于JDK1.8)

    上一篇文章简单的写了一下,BoundedConcurrentHashMap,觉得https://www.cnblogs.com/qiaoyutao/p/10903813.html用的并不多:今天着重写 ...

  8. 【JUC】JDK1.8源码分析之ConcurrentHashMap(一)

    一.前言 最近几天忙着做点别的东西,今天终于有时间分析源码了,看源码感觉很爽,并且发现ConcurrentHashMap在JDK1.8版本与之前的版本在并发控制上存在很大的差别,很有必要进行认真的分析 ...

  9. ConcurrentHashmap详解以及在JDK1.8的更新

    因为hashmap本身是非线程安全的,如果多线程对hashmap进行put操作的话,就会导致死循环等现象.ConcurrentHashMap主要就是为了应对hashmap在并发环境下不安全而诞生的,C ...

随机推荐

  1. lua 脚本之高级篇 (面向对象的完全支持),有性能问题。

    ---------------------------------------------------------- --面向对象核心库 ------------------------------- ...

  2. 十. 图形界面(GUI)设计12.滚动条

    滚动条(JScrollBar)也称为滑块,用来表示一个相对值,该值代表指定范围内的一个整数.例如,用Word编辑文档时,编辑窗右边的滑块对应当前编辑位置在整个文档中的相对位置,可以通过移动选择新的编辑 ...

  3. JNI之数据类型

    1. JNIEnv 作用 JNIEnv 概念 : 是一个线程相关的结构体, 该结构体代表了 Java 在本线程的运行环境 ; JNIEnv 与 JavaVM : 注意区分这两个概念; -- JavaV ...

  4. 如何提高码农产量,基于ASP.NET MVC的敏捷开发框架之移动端开发随笔二

    前言 在前一篇文章中我已经做过开篇,接下来的随笔会详细讲一下我们的开发框架是如何实现的,专业的事由专业的人来讲,以后就由我们的高级码农小李英文名查尔斯和他的师父厂长(因为姓陈,酷爱摄影,我们的文艺片都 ...

  5. 更合理的 setState()

    原文发表在我的博客:http://www.erichain.me/2017/04/17/2017-04-17-more-reasonable-setstate/ React 是我做前端以来接触到的第三 ...

  6. Redis编程实践【pub/sub】

    原文:http://shift-alt-ctrl.iteye.com/blog/1867454 Redis或许已经在很多企业开始推广并试水,本文也根据个人的实践,简单描述一下Redis在实际开发过程中 ...

  7. PostgreSQL配置文件--复制

    4 复制 REPLICATION 4.1 Sending Server(s) 4.1.1 max_wal_senders 数字型 默认: max_wal_senders = 10 , 为0表示启用流复 ...

  8. C/C++ Windows移植到Linux

    近期写了有关Socket的程序,需要从windows移植到linux.现把有用的东东收集整理记录下来. 1.头文件windows下winsock.h或winsock2.h:linux下netinet/ ...

  9. 初识JdbcTemplate

    1.spring配置文件里注冊:參照使用 Spring jdbcTemplate 进一步简化 JDBC 操作 2.写javabean 3.写rowmapper(依据javabean来封装结果集) 4. ...

  10. nagios监控redis

    nagios是非常强大的监控工具,但是它本身没有监控redis的功能 但是网上有很多大神写了监控redis的插件,比较热门的使用perl写的check_redis.pl 但是由于我们监控mongodb ...