「ConcurrentHashMap」深入浅出的源码分析(JDK1.8版本)

ConcurrentHashMap是concurrent家族中的一个类,由于它可以高效地支持并发操作,以及被广泛使用,经典的开源框架Spring的底层数据结构就是使用ConcurrentHashMap实现的。

与同是线程安全的老大哥HashTable相比,它已经更胜一筹,因此它的锁更加细化,而不是像HashTable一样为几乎每个方法都添加了synchronized锁,这样的锁无疑会影响到性能。

本文的分析的源码是JDK8的版本,与JDK7的版本有很大的差异。实现线程安全的思想也已经完全变了,它摒弃了Segment(锁段)的概念,而是启用了一种全新的方式实现,利用CAS算法。

它沿用了与它同时期(JDK1.8)的HashMap版本的思想,底层依然由“数组”+链表+红黑树的方式思想,但是为了做到并发,又增加了很多辅助的类例如TreeBin,Traverser等对象内部类

首先来看几个重要的属性,与HashMap相同的就不再介绍了,这里重点解释一下sizeCtl这个属性。可以说它是ConcurrentHashMap中出镜率很高的一个属性,因为它是一个控制标识符,在不同的地方有不同用途,而且它的取值不同,也代表不同的含义。

  • ** 负数代表正在进行初始化或扩容操作**
  • -1 代表正在初始化
  • -N 表示有N-1个线程正在进行扩容操作
  • 正数或0代表hash表还没有被初始化,这个数值表示初始化或下一次进行扩容的大小,这一点类似于扩容阈值的概念

还后面可以看到,它的值始终是当前ConcurrentHashMap容量的0.75倍,这与loadfactor是对应的。

Node是最核心的内部类,它包装了key-value键值对,所有插入ConcurrentHashMap的数据都包装在这里面。

如下所示:

这个Node内部类与HashMap中定义的Node类很相似

  • 但是有一些差别它对value和next属性设置了volatile的sync锁
  • 它不允许调用setValue方法直接改变Node的value域
  • 它增加了find方法辅助map.get()方法
  • 树节点类,另外一个核心的数据结构
  • 当链表长度过长的时候,会转换为TreeNode。

但是与HashMap不相同的是,它并不是直接转换为红黑树,而是把这些结点包装成TreeNode放在TreeBin对象中,由TreeBin完成对红黑树的包装。

而且TreeNode在ConcurrentHashMap集成自Node类,而并非HashMap中的集成自LinkedHashMap.Entry<K,V>类,也就是说TreeNode带有next指针,这样做的目的是方便基于TreeBin的访问。

这个类并不负责包装用户的key、value信息,而是包装的很多TreeNode节点。它代替了TreeNode的根节点,也就是说在实际的ConcurrentHashMap“数组”中,存放的是TreeBin对象,而不是TreeNode对象,这是与HashMap的区别。另外这个类还带有了读写锁。

可以看到在构造TreeBin节点时,仅仅指定了它的hash值为TREEBIN常量,这也就是个标识为。同时也看到我们熟悉的红黑树构造方法

一个用于连接两个table的节点类。它包含一个nextTable指针,用于指向下一张表。而且这个节点的key value next指针全部为null,它的hash值为-1. 这里面定义的find的方法是从nextTable里进行查询节点,而不是以自身为头节点进行查找

在ConcurrentHashMap中,随处可以看到Unsafe, 大量使用了Unsafe.compareAndSwapXXX的方法,这个方法是利用一个CAS算法实现无锁化的修改值的操作,他可以大大降低锁代理的性能消耗。

这个算法的基本思想就是不断地去比较当前内存中的变量值与你指定的一个变量值是否相等,如果相等,则接受你指定的修改的值,否则拒绝你的操作。

因为当前线程中的值已经不是最新的值,你的修改很可能会覆盖掉其他线程修改的结果。这一点与乐观锁,SVN的思想是比较类似的。

unsafe代码块控制了一些属性的修改工作,比如最常用的SIZECTL 。 在这一版本的concurrentHashMap中,大量应用来的CAS方法进行变量、属性的修改工作。 利用CAS进行无锁操作,可以大大提高性能。

ConcurrentHashMap定义了三个原子操作,用于对指定位置的节点进行操作。正是这些原子操作保证了ConcurrentHashMap的线程安全。

  • 利用CAS算法设置i位置上的Node节点。之所以能实现并发是因为他指定了原来这个节点的值是多少
  • 在CAS算法中,会比较内存中的值与你指定的这个值是否相等,如果相等才接受你的修改,否则拒绝你的修改
  • 因此当前线程中的值并不是最新的值,这种修改可能会覆盖掉其他线程的修改结果 有点类似于SVN
  • 对于ConcurrentHashMap来说,调用它的构造方法仅仅是设置一些参数。
  • 整个table的初始化是在向ConcurrentHashMap中插入元素的时候发生的。
  • 如调用put、computeIfAbsent、compute、merge等方法的时候,调用时机是检查table==null。
  1. 初始化方法主要应用了关键属性sizeCtl 如果这个值 <0,表示其他线程正在进行初始化,就放弃这个操作。
  2. 在这也可以看出ConcurrentHashMap的初始化只能由一个线程完成。
  3. 如果获得了初始化权限,就用CAS方法将sizeCtl置为-1,防止其他线程进入。

初始化数组后,将sizeCtl的值改为0.75 * n,源码如下:

  • ConcurrentHashMap容量不足的时候,需要对table进行扩容。
  • 这个方法的基本思想跟HashMap是很像的,但是由于它是支持并发扩容的,所以要复杂的多。
  • 原因是它支持多线程进行扩容操作,而并没有加锁。
  • 我想这样做的目的不仅仅是为了满足concurrent的要求,而是希望利用并发处理去减少扩容带来的时间影响。因为在扩容的时候,总是会涉及到从一个“数组”到另一个“数组”拷贝的操作,如果这个操作能够并发进行,那真真是极好的了。
  • 第一部分是构建一个nextTable,它的容量是原来的两倍,这个操作是单线程完成的。这个单线程的保证是通过RESIZE_STAMP_SHIFT这个常量经过一次运算来保证的,这个地方在后面会有提到;
  • 第二个部分就是将原来table中的元素复制到nextTable中,这里允许多线程进行操作
  • 它的大体思想就是遍历、复制的过程。首先根据运算得到需要遍历的次数i,然后利用tabAt方法获得i位置的元素:
  • 如果这个位置为空,就在原table中的i位置放入forwardNode节点,这个也是触发并发扩容的关键点
  • 如果这个位置是Node节点(fh>=0),如果它是一个链表的头节点,就构造一个反序链表,把他们分别放在nextTable的i和i+n的位置上
  • 如果这个位置是TreeBin节点(fh<0),也做一个反序处理,并且判断是否需要untreefi,把处理的结果分别放在nextTable的i和i+n的位置上
  • 遍历过所有的节点以后就完成了复制工作,这时让nextTable作为新的table,并且更新sizeCtl为新容量的0.75倍 ,完成扩容
  • 如果遍历到的节点是forward节点,就向后继续遍历,再加上给节点上锁的机制,就完成了多线程的控制。
  • 多线程遍历节点,处理了一个节点,就把对应点的值set为forward,另一个线程看到forward,就向后遍历。
  • 这样交叉就完成了复制工作。而且还很好的解决了线程安全的问题。 这个方法的设计实在是让我膜拜。

ConcurrentHashMap最常用的就是put和get两个方法

1.根据hash值计算这个新插入的点在table中的位置i,如果i位置是空的,直接放进去,否则进行判断,如果i位置是树节点,按照树的方式插入新的节点,否则把i插入到链表的末尾。

2.ConcurrentHashMap中依然沿用这个思想,有一个最重要的不同点就是ConcurrentHashMap不允许key或value为null值

  • 另外由于涉及到多线程,put方法就要复杂一点。在多线程中可能有以下两个情况

3.如果一个或多个线程正在对ConcurrentHashMap进行扩容操作,当前线程也要进入扩容的操作中

  • 这个扩容的操作之所以能被检测到,是因为transfer方法中在空结点上插入forward节点,如果检测到需要插入的位置被forward节点占有,就帮助进行扩容;
  • 如果检测到要插入的节点是非空且不是forward节点,就对这个节点加锁,这样就保证了线程安全。尽管这个有一些影响效率,但是还是会比hashTable的synchronized要好得多。
  • 整体流程就是首先定义不允许key或value为null的情况放入,对于每一个放入的值,首先利用spread方法对key的hashcode进行一次hash计算,由此来确定这个值在table中的位置

如果这个位置是空的,那么直接放入,而且不需要加锁操作。

如果这个位置存在结点,说明发生了hash碰撞,首先判断这个节点的类型。

如果是链表节点(fh>0),则得到的结点就是hash值相同的节点组成的链表的头节点。

需要依次向后遍历确定这个新加入的值所在位置。

如果遇到hash值与key值都与新加入节点是一致的情况,则只需要更新value值即可。否则依次向后遍历,直到链表尾插入这个结点。 如果加入这个节点以后链表长度大于8,就把这个链表转换成红黑树。如果这个节点的类型已经是树节点的话,直接调用树节点的插入方法进行插入新的值。

这是一个协助扩容的方法。这个方法被调用的时候,当前ConcurrentHashMap一定已经有了nextTable对象,首先拿到这个nextTable对象,调用transfer方法。回看上面的transfer方法可以看到,当本线程进入扩容方法的时候会直接进入复制阶段。

这个方法用于将过长的链表转换为TreeBin对象。但是他并不是直接转换,而是进行一次容量判断,如果容量没有达到转换的要求,直接进行扩容操作并返回;如果满足条件才链表的结构抓换为TreeBin ,这与HashMap不同的是,它并没有把TreeNode直接放入红黑树,而是利用了TreeBin这个小容器来封装所有的TreeNode。

get方法比较简单,给定一个key来确定value的时候,必须满足两个条件 key相同 hash值相同,对于节点可能在链表或树上的情况,需要分别去查找.

对于ConcurrentHashMap来说,这个table里到底装了多少东西其实是个不确定的数量,因为不可能在调用size()方法的时候像GC的“stop the world”一样让其他线程都停下来让你去统计,因此只能说这个数量是个估计值。对于这个估计值,ConcurrentHashMap也是大费周章才计算出来的。

为了统计元素个数,ConcurrentHashMap定义了一些变量和一个内部类

mappingCount与size方法的类似 从Java工程师给出的注释来看,应该使用mappingCount代替size方法 两个方法都没有直接返回basecount 而是统计一次这个值,而这个值其实也是一个大概的数值,因此可能在统计的时候有其他线程正在执行插入或删除操作。

在put方法结尾处调用了addCount方法,把当前ConcurrentHashMap的元素个数+1这个方法一共做了两件事,更新baseCount的值,检测是否进行扩容。

感谢阅读,关注+三连是最大的支持!

Java注解最全详解(超级详细)

Java注解是一个很重要的知识点,用于对代码进行说明,可以对包、类、接口、字段、方法参数、局部变量等进行注解。

掌握好Java注解有利于学习框架底层实现。@mikechen

Java注解又称Java标注,是在 JDK5 时引入的新特性,注解(也被称为元数据)。

Java注解它提供了一种安全的类似注释的机制,用来将任何的信息或元数据(metadata)与程序元素(类、方法、成员变量等)进行关联。

Java注解是附加在代码中的一些元信息,用于一些工具在编译、运行时进行解析和使用,起到说明、配置的功能。

1.生成文档这是最常见的,也是java 最早提供的注解;

2.在编译时进行格式检查,如@Override放在方法前,如果你这个方法并不是覆盖了超类方法,则编译时就能检查出;

3.跟踪代码依赖性,实现替代配置文件功能,比较常见的是spring 2.5 开始的基于注解配置,作用就是减少配置;

4.在反射的 Class, Method, Field 等函数中,有许多于 Annotation 相关的接口,可以在反射中解析并使用 Annotation。

包括@Override、@Deprecated、@SuppressWarnings等,使用这些注解后编译器就会进行检查。

元注解是用于定义注解的注解,包括@Retention、@Target、@Inherited、@Documented、@Repeatable 等。元注解也是Java自带的标准注解,只不过用于修饰注解,比较特殊。

用户可以根据自己的需求定义注解。

JDK 中内置了以解:

如果试图使用 @Override 标记一个实际上并没有覆写父类的方法时,java 编译器会告警。

@SuppressWarnings 用于关闭对类、方法、成员编译时产生的特定警告。

1)抑制单类型的警告

2)抑制多类型的警告

3)抑制所有类型的警告

@SuppressWarnings 注解的常见参数值的简单说明:

@FunctionalInterface 用于指示被修饰的接口是函数式接口,在 JDK8 引入。

函数式接口(Functional Interface)就是一个有且仅有一个抽象方法,但是可以有多个非抽象方法的接口。

元注解是java API提供的,是用于修饰注解的注解,通常用在注解的定义上:

@ Retention用来定义该注解在哪一个级别可用,在源代码中(SOURCE)、类文件中(CLASS)或者运行时(RUNTIME)。

@Retention 源码:

RetentionPolicy 是一个枚举类型,它定义了被 @Retention 修饰的注解所支持的保留级别:

@Documented:生成文档信息的时候保留注解,对类作辅助说明

@Documented 示例

@Target:用于描述注解的使用范围(即:被描述的注解可以用在什么地方)

@Target源码:

ElementType 是一个枚举类型,它定义了被 @Target 修饰的注解可以应用的范围:

@Inherited:说明子类可以继承父类中的该注解

表示自动继承注解类型。 如果注解类型声明中存在 @Inherited 元注解,则注解所修饰类的所有子类都将会继承此注解。

@Repeatable 表示注解可以重复使用。

当我们需要重复使用某个注解时,希望利用相同的注解来表现所有的形式时,我们可以借助@Repeatable注解。以 Spring @Scheduled 为例:

如果不满足于文章详解,私信【架构】获取视频详解!

本文作者及来源:Renderbus瑞云渲染农场https://www.renderbus.com

点赞 0
收藏 0

文章为作者独立观点不代本网立场,未经允许不得转载。