前言

JDK1.7&1.8源码对比分析【集合】HashMap中我们对比分析了JDK1.7和1.8版本的HashMap源码,趁热打铁,这篇文章就来看看JDK1.7和1.8版本的ConcurrentHashMap有哪些区别。

目录

一、对比分析

1. 1.7版本

2. 1.8版本

一、对比分析

1. 1.7版本

先来看看 1.7 的实现,下面是他的结构图:

如上图所示,是由 Segment 数组、HashEntry 组成,和 HashMap 一样,仍然是数组 + 链表

它的核心成员变量:

  1. /**
  2. * The segments, each of which is a specialized hash table.
  3. * Segment 数组,存放数据时首先需要定位到具体的 Segment 中
  4. */
  5. final Segment<K,V>[] segments;
  6.  
  7. transient Set<K> keySet;
  8. transient Set<Map.Entry<K,V>> entrySet;

Segment 是 ConcurrentHashMap 的一个内部类,主要的组成如下:

  1. static final class Segment<K,V> extends ReentrantLock implements Serializable {
  2.  
  3. private static final long serialVersionUID = 2249069246763182397L;
  4.  
  5. /**
  6. * The maximum number of times to tryLock in a prescan before
  7. * possibly blocking on acquire in preparation for a locked
  8. * segment operation. On multiprocessors, using a bounded
  9. * number of retries maintains cache acquired while locating
  10. * nodes.
  11. */
  12. static final int MAX_SCAN_RETRIES =
  13. Runtime.getRuntime().availableProcessors() > 1 ? 64 : 1;
  14.  
  15. /**
  16. * The per-segment table. Elements are accessed via
  17. * entryAt/setEntryAt providing volatile semantics.
  18. * 和 HashMap 中的 HashEntry 作用一样,真正存放数据的桶
  19. */
  20. transient volatile HashEntry<K,V>[] table;
  21.  
  22. /**
  23. * The number of elements. Accessed only either within locks
  24. * or among other volatile reads that maintain visibility.
  25. */
  26. transient int count;
  27.  
  28. /**
  29. * The total number of mutative operations in this segment.
  30. * Even though this may overflows 32 bits, it provides
  31. * sufficient accuracy for stability checks in CHM isEmpty()
  32. * and size() methods. Accessed only either within locks or
  33. * among other volatile reads that maintain visibility.
  34. */
  35. transient int modCount;
  36.  
  37. /**
  38. * The table is rehashed when its size exceeds this threshold.
  39. * (The value of this field is always <tt>(int)(capacity *
  40. * loadFactor)</tt>.)
  41. */
  42. transient int threshold;
  43.  
  44. /**
  45. * The load factor for the hash table. Even though this value
  46. * is same for all segments, it is replicated to avoid needing
  47. * links to outer object.
  48. * @serial
  49. */
  50. final float loadFactor;
  51. }

看看其中 HashEntry 的组成:

  1. /**
  2. * ConcurrentHashMap list entry. Note that this is never exported
  3. * out as a user-visible Map.Entry.
  4. */
  5. static final class HashEntry<K,V> {
  6. final int hash;
  7. final K key;
  8. volatile V value;
  9. volatile HashEntry<K,V> next;
  10.  
  11. HashEntry(int hash, K key, V value, HashEntry<K,V> next) {
  12. this.hash = hash;
  13. this.key = key;
  14. this.value = value;
  15. this.next = next;
  16. }
  17. }

和 HashMap 非常类似,唯一的区别就是其中的核心数据如 value ,以及链表都是 volatile 修饰的,保证了获取时的可见性。

原理上来说:ConcurrentHashMap 采用了分段锁技术,其中 Segment 继承于 ReentrantLock。不会像 HashTable 那样不管是 put 还是 get 操作都需要做同步处理,理论上 ConcurrentHashMap 支持 CurrencyLevel (Segment 数组数量)的线程并发。每当一个线程占用锁访问一个 Segment 时,不会影响到其他的 Segment。

下面也来看看核心的put、get方法。

put方法

  1. @SuppressWarnings("unchecked")
  2. public V put(K key, V value) {
  3. Segment<K,V> s;
  4. if (value == null)
  5. throw new NullPointerException();
  6. int hash = hash(key);
  7. int j = (hash >>> segmentShift) & segmentMask;
  8. if ((s = (Segment<K,V>)UNSAFE.getObject // nonvolatile; recheck
  9. (segments, (j << SSHIFT) + SBASE)) == null) // in ensureSegment
  10. s = ensureSegment(j);
  11. return s.put(key, hash, value, false);
  12. }
  13.  
  14. final V put(K key, int hash, V value, boolean onlyIfAbsent) {
  15. HashEntry<K,V> node = tryLock() ? null :
  16. scanAndLockForPut(key, hash, value);
  17. V oldValue;
  18. try {
  19. HashEntry<K,V>[] tab = table;
  20. int index = (tab.length - 1) & hash;
  21. HashEntry<K,V> first = entryAt(tab, index);
  22. for (HashEntry<K,V> e = first;;) {
  23. if (e != null) {
  24. K k;
  25. if ((k = e.key) == key ||
  26. (e.hash == hash && key.equals(k))) {
  27. oldValue = e.value;
  28. if (!onlyIfAbsent) {
  29. e.value = value;
  30. ++modCount;
  31. }
  32. break;
  33. }
  34. e = e.next;
  35. }
  36. else {
  37. if (node != null)
  38. node.setNext(first);
  39. else
  40. node = new HashEntry<K,V>(hash, key, value, first);
  41. int c = count + 1;
  42. if (c > threshold && tab.length < MAXIMUM_CAPACITY)
  43. rehash(node);
  44. else
  45. setEntryAt(tab, index, node);
  46. ++modCount;
  47. count = c;
  48. oldValue = null;
  49. break;
  50. }
  51. }
  52. } finally {
  53. unlock();
  54. }
  55. return oldValue;
  56. }

虽然 HashEntry 中的 value 是用 volatile 关键词修饰的,但是并不能保证并发的原子性,所以 put 操作时仍然需要加锁处理。

首先第一步的时候会尝试获取锁,如果获取失败肯定就有其他线程存在竞争,则利用scanAndLockForPut()自旋获取锁。

  1. private HashEntry<K,V> scanAndLockForPut(K key, int hash, V value) {
  2. HashEntry<K,V> first = entryForHash(this, hash);
  3. HashEntry<K,V> e = first;
  4. HashEntry<K,V> node = null;
  5. int retries = -1; // negative while locating node
  6. while (!tryLock()) { // 尝试自旋获取锁
  7. HashEntry<K,V> f; // to recheck first below
  8. if (retries < 0) {
  9. if (e == null) {
  10. if (node == null) // speculatively create node
  11. node = new HashEntry<K,V>(hash, key, value, null);
  12. retries = 0;
  13. }
  14. else if (key.equals(e.key))
  15. retries = 0;
  16. else
  17. e = e.next;
  18. }
  19. else if (++retries > MAX_SCAN_RETRIES) {
  20. // 如果重试的次数达到了 MAX_SCAN_RETRIES 则改为阻塞锁获取,保证能获取成功
  21. lock();
  22. break;
  23. }
  24. else if ((retries & 1) == 0 &&
  25. (f = entryForHash(this, hash)) != first) {
  26. e = first = f; // re-traverse if entry changed
  27. retries = -1;
  28. }
  29. }
  30. return node;
  31. }

再看看put的流程:

  1. final V put(K key, int hash, V value, boolean onlyIfAbsent) {
  2. HashEntry<K,V> node = tryLock() ? null :
  3. // 1. 将当前 Segment 中的 table 通过 key 的 hashcode 定位到 HashEntry
  4. scanAndLockForPut(key, hash, value);
  5. V oldValue;
  6. try {
  7. HashEntry<K,V>[] tab = table;
  8. int index = (tab.length - 1) & hash;
  9. HashEntry<K,V> first = entryAt(tab, index);
  10. for (HashEntry<K,V> e = first;;) {
  11. if (e != null) {
  12. K k;
  13. // 2. 遍历该 HashEntry,如果不为空则判断传入的 key 和当前遍历的 key 是否相等,相等则覆盖旧的 value
  14. if ((k = e.key) == key ||
  15. (e.hash == hash && key.equals(k))) {
  16. oldValue = e.value;
  17. if (!onlyIfAbsent) {
  18. e.value = value;
  19. ++modCount;
  20. }
  21. break;
  22. }
  23. e = e.next;
  24. }
  25. // 3. 不为空则需要新建一个 HashEntry 并加入到 Segment 中,同时会先判断是否需要扩容
  26. else {
  27. if (node != null)
  28. node.setNext(first);
  29. else
  30. node = new HashEntry<K,V>(hash, key, value, first);
  31. int c = count + 1;
  32. if (c > threshold && tab.length < MAXIMUM_CAPACITY)
  33. rehash(node);
  34. else
  35. setEntryAt(tab, index, node);
  36. ++modCount;
  37. count = c;
  38. oldValue = null;
  39. break;
  40. }
  41. }
  42. } finally {
  43. // 4. 最后会解除在 1 中所获取当前 Segment 的锁
  44. unlock();
  45. }
  46. return oldValue;
  47. }

get方法

  1. public V get(Object key) {
  2. Segment<K,V> s; // manually integrate access methods to reduce overhead
  3. HashEntry<K,V>[] tab;
  4. int h = hash(key);
  5. long u = (((h >>> segmentShift) & segmentMask) << SSHIFT) + SBASE;
  6. if ((s = (Segment<K,V>)UNSAFE.getObjectVolatile(segments, u)) != null &&
  7. (tab = s.table) != null) {
  8. for (HashEntry<K,V> e = (HashEntry<K,V>) UNSAFE.getObjectVolatile
  9. (tab, ((long)(((tab.length - 1) & h)) << TSHIFT) + TBASE);
  10. e != null; e = e.next) {
  11. K k;
  12. if ((k = e.key) == key || (e.hash == h && key.equals(k)))
  13. return e.value;
  14. }
  15. }
  16. return null;
  17. }

get 逻辑比较简单:

只需要将 Key 通过 Hash 之后定位到具体的 Segment ,再通过一次 Hash 定位到具体的元素上。

由于 HashEntry 中的 value 属性是用 volatile 关键词修饰的,保证了内存可见性,所以每次获取时都是最新值。

ConcurrentHashMap 的 get 方法是非常高效的,因为整个过程都不需要加锁

2. 1.8版本

1.7 已经解决了并发问题,并且能支持 N 个 Segment 这么多次数的并发,但依然存在 HashMap 在 1.7 版本中的问题。那就是查询遍历链表效率太低。

因此 1.8 做了一些数据结构上的调整。首先来看下底层的组成结构:

看起来是不是和 1.8 HashMap 结构类似?

其中抛弃了原有的 Segment 分段锁,而采用了CAS + synchronized来保证并发安全性。

  1. static class Node<K,V> implements Map.Entry<K,V> {
  2. final int hash;
  3. final K key;
  4. volatile V val;
  5. volatile Node<K,V> next;
  6.  
  7. Node(int hash, K key, V val, Node<K,V> next) {
  8. this.hash = hash;
  9. this.key = key;
  10. this.val = val;
  11. this.next = next;
  12. }
  13.  
  14. public final K getKey() { return key; }
  15. public final V getValue() { return val; }
  16. public final int hashCode() { return key.hashCode() ^ val.hashCode(); }
  17. public final String toString(){ return key + "=" + val; }
  18. public final V setValue(V value) {
  19. throw new UnsupportedOperationException();
  20. }
  21.  
  22. public final boolean equals(Object o) {
  23. Object k, v, u; Map.Entry<?,?> e;
  24. return ((o instanceof Map.Entry) &&
  25. (k = (e = (Map.Entry<?,?>)o).getKey()) != null &&
  26. (v = e.getValue()) != null &&
  27. (k == key || k.equals(key)) &&
  28. (v == (u = val) || v.equals(u)));
  29. }
  30.  
  31. /**
  32. * Virtualized support for map.get(); overridden in subclasses.
  33. */
  34. Node<K,V> find(int h, Object k) {
  35. Node<K,V> e = this;
  36. if (k != null) {
  37. do {
  38. K ek;
  39. if (e.hash == h &&
  40. ((ek = e.key) == k || (ek != null && k.equals(ek))))
  41. return e;
  42. } while ((e = e.next) != null);
  43. }
  44. return null;
  45. }
  46. }

也将 1.7 中存放数据的 HashEntry 改为 Node,但作用都是相同的。其中的val、next都用了 volatile 修饰,保证了可见性。

put方法

重点来看看 put 函数:

  1. public V put(K key, V value) {
  2. return putVal(key, value, false);
  3. }
  4.  
  5. /** Implementation for put and putIfAbsent */
  6. final V putVal(K key, V value, boolean onlyIfAbsent) {
  7. if (key == null || value == null) throw new NullPointerException();
  8. // 根据 key 计算出 hashcode
  9. int hash = spread(key.hashCode());
  10. int binCount = 0;
  11. for (Node<K,V>[] tab = table;;) {
  12. Node<K,V> f; int n, i, fh;
  13. // 判断是否需要进行初始化
  14. if (tab == null || (n = tab.length) == 0)
  15. tab = initTable();
  16. // f 即为当前 key 定位出的 Node,如果为空表示当前位置可以写入数据,利用 CAS 尝试写入,失败则自旋保证成功
  17. else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
  18. if (casTabAt(tab, i, null,
  19. new Node<K,V>(hash, key, value, null)))
  20. break; // no lock when adding to empty bin
  21. }
  22. // 如果当前位置的 hashcode == MOVED == -1,则需要进行扩容
  23. else if ((fh = f.hash) == MOVED)
  24. tab = helpTransfer(tab, f);
  25. else {
  26. V oldVal = null;
  27. synchronized (f) {// 如果都不满足,则利用 synchronized 锁写入数据
  28. if (tabAt(tab, i) == f) {
  29. if (fh >= 0) {
  30. binCount = 1;
  31. for (Node<K,V> e = f;; ++binCount) {
  32. K ek;
  33. if (e.hash == hash &&
  34. ((ek = e.key) == key ||
  35. (ek != null && key.equals(ek)))) {
  36. oldVal = e.val;
  37. if (!onlyIfAbsent)
  38. e.val = value;
  39. break;
  40. }
  41. Node<K,V> pred = e;
  42. if ((e = e.next) == null) {
  43. pred.next = new Node<K,V>(hash, key,
  44. value, null);
  45. break;
  46. }
  47. }
  48. }
  49. else if (f instanceof TreeBin) {
  50. Node<K,V> p;
  51. binCount = 2;
  52. if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
  53. value)) != null) {
  54. oldVal = p.val;
  55. if (!onlyIfAbsent)
  56. p.val = value;
  57. }
  58. }
  59. }
  60. }
  61. if (binCount != 0) {
  62. // 如果数量大于 TREEIFY_THRESHOLD 则要转换为红黑树
  63. if (binCount >= TREEIFY_THRESHOLD)
  64. treeifyBin(tab, i);
  65. if (oldVal != null)
  66. return oldVal;
  67. break;
  68. }
  69. }
  70. }
  71. addCount(1L, binCount);
  72. return null;
  73. }

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());
  4. if ((tab = table) != null && (n = tab.length) > 0 &&
  5. (e = tabAt(tab, (n - 1) & h)) != null) {
  6. // 根据计算出来的 hashcode 寻址,如果就在桶上那么直接返回值
  7. if ((eh = e.hash) == h) {
  8. if ((ek = e.key) == key || (ek != null && key.equals(ek)))
  9. return e.val;
  10. }
  11. // 如果是红黑树那就按照树的方式获取值
  12. else if (eh < 0)
  13. return (p = e.find(h, key)) != null ? p.val : null;
  14. // 都不满足那就按照链表的方式遍历获取值
  15. while ((e = e.next) != null) {
  16. if (e.hash == h &&
  17. ((ek = e.key) == key || (ek != null && key.equals(ek))))
  18. return e.val;
  19. }
  20. }
  21. return null;
  22. }

1.8 在 1.7 的数据结构上做了大的改动,采用红黑树之后可以保证查询效率(O(logn)),甚至取消了 ReentrantLock 改为了 synchronized,这样可以看出在新版的 JDK 中对 synchronized 优化是很到位的。

JDK(十)JDK1.7&1.8源码对比分析【集合】ConcurrentHashMap的更多相关文章

  1. JDK(八)JDK1.7&1.8源码对比分析【集合】HashMap

    前言 在JDK1.8源码分析[集合]HashMap文章中,我们分析了HashMap在JDK1.8中新增的特性(引进了红黑树数据结构),但是为什么要进行这个优化呢?这篇文章我们通过对比JDK1.7和1. ...

  2. LinkedHashMap 源码详细分析(JDK1.8)

    1. 概述 LinkedHashMap 继承自 HashMap,在 HashMap 基础上,通过维护一条双向链表,解决了 HashMap 不能随时保持遍历顺序和插入顺序一致的问题.除此之外,Linke ...

  3. 阿里P7终于讲完了JDK+Spring+mybatis+Dubbo+SpringMvc+Netty源码

    前言 这里普及一下,每个公司都有职别定级系统,阿里也是,技术岗以 P 定级,一般校招 P5, 社招 P6 起.其实阅读源码也是有很多诀窍的,这里分享几点心得: 首先要会用.你要知道这个库是干什么的,掌 ...

  4. Java -- 基于JDK1.8的ArrayList源码分析

    1,前言 很久没有写博客了,很想念大家,18年都快过完了,才开始写第一篇,争取后面每周写点,权当是记录,因为最近在看JDK的Collection,而且ArrayList源码这一块也经常被面试官问道,所 ...

  5. HashMap 与 ConcrrentHashMap 使用以及源码原理分析

    前奏一:HashMap面试中常见问题汇总 HashMap的工作原理是近年来常见的Java面试题,几乎每个Java程序员都知道HashMap,都知道哪里要用HashMap,知道HashTable和Has ...

  6. JUC同步器框架AbstractQueuedSynchronizer源码图文分析

    JUC同步器框架AbstractQueuedSynchronizer源码图文分析 前提 Doug Lea大神在编写JUC(java.util.concurrent)包的时候引入了java.util.c ...

  7. MyBatis源码骨架分析

    源码包分析 MyBatis 源码下载地址:https://github.com/MyBatis/MyBatis-3 MyBatis源码导入过程: 下载MyBatis的源码 检查maven的版本,必须是 ...

  8. TaskTracker任务初始化及启动task源码级分析

    在监听器初始化Job.JobTracker相应TaskTracker心跳.调度器分配task源码级分析中我们分析的Tasktracker发送心跳的机制,这一节我们分析TaskTracker接受JobT ...

  9. [Java] Hashtable 源码简要分析

    Hashtable /HashMap / LinkedHashMap 概述 * Hashtable比较早,是线程安全的哈希映射表.内部采用Entry[]数组,每个Entry均可作为链表的头,用来解决冲 ...

随机推荐

  1. Java基础学习—思维导图

    找到两张Java学习的思维导图,特别适合我这样的菜鸟学习,贴过来和小伙伴分享.

  2. 【小程序】返回顶部wx.pageScrollTo和scroll-view的对比

    一.wx.pageScrollTo(https://mp.weixin.qq.com/debug/wxadoc/dev/api/scroll.html) 1. 小程序中双击顶部的textbar.会默认 ...

  3. How to download a CRX file from the Chrome web store

    如何从 谷歌浏览器商店 离线下载 谷歌浏览器扩展 Simply copying the Chrome store extension url to the following website: htt ...

  4. 你真的了解View的坐标吗?

    闲聊 View,对我们来说在熟悉不过了,从接触 Android 开始,我们就一直在接触 View,界面当中到处都是 View,比如我们经常用到的 TextView,Button,LinearLayou ...

  5. c#实现内存映射文件共享内存

    原文:http://blog.csdn.net/wangtiewei/article/details/51112668 内存映射文件是利用虚拟内存把文件映射到进程的地址空间中去,在此之后进程操作文件, ...

  6. JSON学习笔记-2

    JSON的语法 1.JSON 数据的书写格式是:名称/值对. "name" : "我是一个菜鸟" 等价于这条 JavaScript 语句: name = &qu ...

  7. RecyclerView中notifyDataSetChanged刷新总结

    除了adapter.notifyDataSetChanged()这个方法之外,新的Adapter还提供了其他的方法,如下: public final void notifyDataSetChanged ...

  8. java基础(四) java运算顺序的深入解析

    1. 从左往右的计算顺序   与C/C++不同的是,在Java中,表达式的计算与结果是确定的,不受硬件与环境的影响.如: int i = 5; int j = (i++) + (i++) +(i++) ...

  9. Linux 查看双节点是否做了SSH信任

    perl $AD_TOP/patch/115/bin/txkRunSSHSetup.pl verifyssh -contextfile=$CONTEXT_FILE -hosts=erpapp1,erp ...

  10. Oracle EBS PO 接受入库