JDK1.8 ConcurrentHashMap源码阅读

1.  带着问题去阅读

为什么说ConcurrentHashMap是线程安全的?或者说 ConcurrentHashMap是如何防止并发的?

2.  字段和常量

首先,来看一下ConcurrentHashMap中的一些字段和常量,这些在接下来的操作中会用得到

2.1.  常量

从中,我们可以获得以下信息:

  1. 数组的默认容量是16,最大容量是1<<30
  2. 当添加元素的时候,将列表转成树的阈值是8。也就是说,相同位置上多个元素是以链表的形式存储的,而当链表的长度(元素的个数)超过8时,将其转为树
  3. 在对数组扩容的时候,当树中元素个数小于或等于6时,将树转成链表

2.2.  字段

从这些字段中,我们可以获得以下信息:

  1. 底层是一个数组,且数组的类型是Node,延迟初始化,更重要的是它被 volatile 修饰
  2. sizeCtl是用于数组初始化和扩容的,当它是负数的时候,表示数组正在进行初始化或扩容,-1表示正在初始化,同时应该注意到它也被 volatile 修饰

2.3.  内部类

对比1.7里面的HashMap不难发现:

  1. Node继承自Map.Entry
  2. 其 value 和 next 都用 volatile 修饰

可以看到,TreeNode继承自Node,主要用于树形结构中。也就是说,TreeNode表示树中的结点。

还有一个TreeBin也是继承自Node

TreeBin表示整个树,TreeNode表示树中的结点

正常情况下,数组中某个位置的元素应该是Node,而Node是一个链表,它后面可能跟了多个Node。

但是,某个位置的节点个数超过阈值(默认8)时,将这个链表转成红黑树,那么此后数组中这个位置的元素就是TreeBin

也就是说,Node表示链表中的节点,TreeNode表示树中的节点,TreeBin表示树

3.  操作

3.1.  put

这里,再多看一眼,刚才的putTreeVal()方法

总的来说,是先插入,后调整

大致流程是这样的:

  1. 如果数组为空,则先初始化数组
  2. 根据key计算哈希值,进而计算应该在数组的什么位置
  3. 取出该位置上的元素,如果为空,则直接构造一个Node,并将元素放置于此
  4. 如果该位置上的元素不为空,则进一步判断是链表还是树(PS:Node还是TreeBin)
  5. 如果是Node,则遍历链表,如果发现有key相同的元素,则用新值替换旧值,否则构造Node,并将其插入到链表尾部
  6. 如果是TreeBin,则遍历树,若发现相同key的节点,则用新值替换旧值,否则构造TreeNode,并将其插入到树中
  7. 插入完成以后,最后再看一下要不要转成树型结构
  8. 如果旧值不为空,则返回旧值

3.2.  resize

在上一步的put操作中,如果数组正在扩容,则帮助扩容

下面看一下扩容

我以前在理解上一直有一个误区,以前我一直以为在数组相同位置上的元素的哈希值都相同,今天我恍然大悟,原来不是这样的,这些元素之所以会在同一个位置是因为通过key的哈希值再结合数组长度计算得出该元素应该在这个位置上,而不同的哈希值可能经过计算也在同一个位置,所以,相同位置的元素的hash值不一定相同,或者说,链表上的元素的hash并不一定都相同,只是恰巧它们在数组的位置相同而已。

扩容是这样的:

  1. 新数组的长度是原来的2倍
  2. 根据不同位置的元素的结构有不同的方式
  3. 不管原来是链表结构还是树型结构,扩容以后都变成两部分,一部分是hash&n为0的,另一部分是hash&n不为0的,其中n为原数组的长度
  4. 对于那些hash&n==0的结点,它们在新数组中的位置保持不变,也就是说它们原先在旧数组中是什么位置,现在在新数组中还是什么位置
  5. 对于那些hash&n != 0的节点,它们在新数组中的位置相比于之前在旧数组中的位置是向后移动了n
  6. 每个位置在迁移的时候都加锁了
  7. 扩容后,原来在旧数组中在相同位置的结点在新数组中未必还在相同的位置
  8. 扩容后,链表没有倒置
  9. 由于迁移到新数组中时,会将原先一棵树分成两部分(跟链表一样),所以分出来的树中如果结点数小于或等于6,则转成链表

下面是一个示意图,不必拘泥细节,重在意思

3.3.  get和remove

删除和获取相对比较简单,不再赘述

至此,可以回答开头我们提出的问题了

sychronized + volatile + CAS

插入、删除、扩容的时候都对数组中相应位置的元素加锁了,加锁用的是synchronized

table数组、Node中的val和next、以及一些控制字段都加了volatile

在更新一些关键变量的时候用到了sun.misc.Unsafe中的一些方法

原文地址:https://www.cnblogs.com/cjsblog/p/10017911.html

时间: 2024-10-27 13:55:23

JDK1.8 ConcurrentHashMap源码阅读的相关文章

Java Jdk1.8 HashMap源码阅读笔记二

三.源码阅读 3.元素包含containsKey(Object key) /** * Returns <tt>true</tt> if this map contains a mapping for the * specified key. * * @param key The key whose presence in this map is to be tested * @return <tt>true</tt> if this map contains

Java Jdk1.8 HashMap源码阅读笔记一

最近在工作用到Map等一系列的集合,于是,想仔细看一下其具体实现. 一.结构 public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable 1.抽象类AbstractMap public abstract class AbstractMap<K,V> implements Map<K,V> 该类实现了Map接口,具体结

【JDK1.8】JDK1.8集合源码阅读——LinkedList

一.前言 这次我们来看一下常见的List中的第二个--LinkedList,在前面分析ArrayList的时候,我们提到,LinkedList是链表的结构,其实它跟我们在分析map的时候讲到的LinkedHashMap的结构有一定的相似,但是相对简单很多,今天再详细的看一下它的具体结构,以及使用的场景等. 二.LinkedList结构概览 在看具体的结构之前我们先来看一下它的继承关系: 与ArrayList不同的是,LinkedList继承了AbstractSequentialList,从Seq

ConcurrentHashMap 源码阅读小结

前言 每一次总结都意味着重新开始,同时也是为了更好的开始.ConcurrentHashMap 一直是我心中的痛.虽然不敢说完全读懂了,但也看了几个重要的方法,有不少我觉得比较重要的知识点. 然后呢,放一些楼主写的关于 ConcurrentHashMap 相关源码分析的文章链接: ConcurrentHashMap 扩容分析拾遗 并发编程--ConcurrentHashMap#addCount() 分析 并发编程--ConcurrentHashMap#transfer() 扩容逐行分析 并发编程-

ConcurrentHashMap源码阅读以及底层实现的简单分析

ConcurrentHashMap 是可以实现多线程并发的HashMap,它是线程安全的. 前面分析过 HashMap的源码,它和HashMap有很多的相同点一样,比如它也有 initialCapacity 以及负载因子 loadFactor 属性.而且他们的默认值也是16和0.75. static final int DEFAULT_INITIAL_CAPACITY =16; static final float DEFAULT_LOAD_FACTOR =0.75f; 和HashMap不同的是

【JDK1.8】JDK1.8集合源码阅读——TreeMap(二)

目录 一.前言二. TreeMap的结构三.Tree源码解析3.1 TreeMap的成员变量3.2 TreeMap的构造方法3.3 TreeMap的重要方法四.总结 一.前言 在前一篇博客中,我们对TreeMap的继承关系进行了分析,在这一篇里,我们将分析TreeMap的数据结构,深入理解它的排序能力是如何实现的.这一节要有一定的数据结构基础,在阅读下面的之前,推荐大家先看一下:<算法4>深入理解红黑树.(个人比较喜欢算法四这里介绍的红黑树实现:从2-3树到红黑树的过渡很清晰,虽然源码里的实现

【JDK1.8】JDK1.8集合源码阅读——Set汇总

一.前言 这一篇里,我将对HashSet.LinkedHashSet.TreeSet进行汇总分析,并不打算一一进行详细介绍,因为JDK对Set的实现进行了取巧.我们都知道Set不允许出现相同的对象,而Map也同样不允许有两个相同的Key(出现相同的时候,就执行更新操作).所以Set里的实现实际上是调用了对应的Map,将Set的存放的对象作为Map的Key. 二.源码分析 这里笔者就以最常用的HashSet为例进行分析,其余的TreeSet.LinkedHashSet类似,就不赘述了. 2.1 结

Java7、8中HashMap和ConcurrentHashMap源码阅读

首先来看下HashMap的类继承结构: public class HashMap extends AbstractMap<K,V> impement Map<K,V>,Coloneable,Serializable{ } 可以看出HashMap实现了Map接口.其里面的方法都是非线程安全的,且不支持并发操作.对于HashMap主要看的是get/put方法实现,其在jdk1.7,及1.8在解决哈希冲突的上有所不同.一.Java7 HashMap 从上面的结构图中,可以大致看出,Has

ConcurrentHashMap 源码详细分析(JDK1.8)

ConcurrentHashMap 源码详细分析(JDK1.8) 1. 概述 <HashMap 源码详细分析(JDK1.8)>:https://segmentfault.com/a/1190000012926722 Java7 整个 ConcurrentHashMap 是一个 Segment 数组,Segment 通过继承 ReentrantLock 来进行加锁,所以每次需要加锁的操作锁住的是一个 segment,这样只要保证每个 Segment 是线程安全的,也就实现了全局的线程安全.所以很