摘录

JDK1.7版本: 容器中有多把锁,每一把锁锁一段数据,这样在多线程访问时不同段的数据时,就不会存在锁竞争了,这 样便可以有效地提高并发效率。这就是ConcurrentHashMap所采用的”分段锁”思想,见下图:

ConcurrentHashmap - 图1
ConcurrentHashmap - 图2
每一个segment都是一个HashEntry[] table, table中的每一个元素本质上都是一个HashEntry的单向队列(原理和hashMap一样)。

JDK1.8版本:做了2点修改,见下图:

  • 取消segments字段,直接采用transient volatile HashEntry[] table保存数据,采用table数组元素作为锁,从而实现了对每一行数据进行加锁,并发控制使用Synchronized和CAS来操作
  • 将原先table数组+单向链表的数据结构,变更为table数组+单向链表+红黑树的结构.

————————————————
版权声明:本文为CSDN博主「快乐小石头」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/weixin_43185598/article/details/87938882

从JDK1.7版本的ReentrantLock+Segment+HashEntry,到JDK1.8版本中synchronized+CAS+HashEntry+红黑树,总结如下:
1、JDK1.7版本锁的粒度是基于Segment的,包含多个HashEntry,而JDK1.8实现降低锁的粒度就是HashEntry(首节点)
2、JDK1.8版本的数据结构变得更加简单,去掉了Segment这种数据结构,使用synchronized来进行同步锁粒度降低,所以不需要分段锁的概念,实现的复杂度也增加了
3、JDK1.8使用红黑树来优化链表,基于长度很长的链表的遍历是一个很漫长的过程,而红黑树的遍历效率是很快的,代替一定阈值的链表,这样形成一个最佳拍档

4、JDK1.8为什么使用内置锁synchronized来代替重入锁ReentrantLock:
- 低粒度加锁方式,synchronized并不比ReentrantLock差,
粗粒度加锁中ReentrantLock可能通过Condition来控制各个低粒度的边界,更加的灵活,而在低粒度中,Condition的优势就没有了
- JVM的开发团队从来都没有放弃synchronized,而且基于JVM的synchronized优化空间更大,使用内嵌的关键字比使用API更加自然
- 在大量的数据操作下,对于JVM的内存压力,基于API的ReentrantLock会开销更多的内存
————————————————
版权声明:本文为CSDN博主「快乐小石头」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/weixin_43185598/article/details/87938882

源码

  1. transient volatile Node<K,V>[] table;
  2. public V put(K key, V value) {
  3. return putVal(key, value, false);
  4. }
  5. final V putVal(K key, V value, boolean onlyIfAbsent) {
  6. if (key == null || value == null) throw new NullPointerException();
  7. // 得到 hash 值
  8. int hash = spread(key.hashCode());
  9. // 用于记录相应链表的长度
  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. // 初始化数组,后面会详细介绍
  16. tab = initTable();
  17. // 找该 hash 值对应的数组下标,得到第一个节点 f
  18. else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
  19. // 如果数组该位置为空,
  20. // 用一次 CAS 操作将这个新值放入其中即可,这个 put 操作差不多就结束了,可以拉到最后面了
  21. // 如果 CAS 失败,那就是有并发操作,进到下一个循环就好了
  22. if (casTabAt(tab, i, null, new Node<K,V>(hash, key, value, null)))
  23. break; // no lock when adding to empty bin
  24. }
  25. // hash 居然可以等于 MOVED,这个需要到后面才能看明白,不过从名字上也能猜到,肯定是因为在扩容
  26. else if ((fh = f.hash) == MOVED)
  27. // 帮助数据迁移,这个等到看完数据迁移部分的介绍后,再理解这个就很简单了
  28. tab = helpTransfer(tab, f);
  29. else { // 到这里就是说,f 是该位置的头结点,而且不为空
  30. V oldVal = null;
  31. // 获取数组该位置的头结点的监视器锁
  32. synchronized (f) {
  33. if (tabAt(tab, i) == f) {
  34. if (fh >= 0) { // 头结点的 hash 值大于 0,说明是链表
  35. // 用于累加,记录链表的长度
  36. binCount = 1;
  37. // 遍历链表
  38. for (Node<K,V> e = f;; ++binCount) {
  39. K ek;
  40. // 如果发现了"相等"的 key,判断是否要进行值覆盖,然后也就可以 break 了
  41. if (e.hash == hash &&
  42. ((ek = e.key) == key || (ek != null && key.equals(ek)))) {
  43. oldVal = e.val;
  44. if (!onlyIfAbsent)
  45. e.val = value;
  46. break;
  47. }
  48. // 到了链表的最末端,将这个新值放到链表的最后面 尾插
  49. Node<K,V> pred = e;
  50. if ((e = e.next) == null) {
  51. pred.next = new Node<K,V>(hash, key, value, null);
  52. break;
  53. }
  54. }
  55. }
  56. else if (f instanceof TreeBin) { // 红黑树
  57. Node<K,V> p;
  58. binCount = 2;
  59. // 调用红黑树的插值方法插入新节点
  60. if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key, value)) != null) {
  61. oldVal = p.val;
  62. if (!onlyIfAbsent)
  63. p.val = value;
  64. }
  65. }
  66. }
  67. }
  68. // binCount != 0 说明上面在做链表操作
  69. if (binCount != 0) {
  70. // 判断是否要将链表转换为红黑树,临界值和 HashMap 一样,也是 8
  71. if (binCount >= TREEIFY_THRESHOLD)
  72. // 这个方法和 HashMap 中稍微有一点点不同,那就是它不是一定会进行红黑树转换,
  73. // 如果当前数组的长度小于 64,那么会选择进行数组扩容,而不是转换为红黑树
  74. // 具体源码我们就不看了,扩容部分后面说
  75. treeifyBin(tab, i);
  76. if (oldVal != null)
  77. return oldVal;
  78. break;
  79. }
  80. }
  81. }
  82. //
  83. addCount(1L, binCount);
  84. return null;
  85. }