后端精进笔记10:浅析ConcurrentHashMap

在分析ConcurrentHashMap之前,先来看看基础款的HashMap源码是什么样的

一、HashMap

1.1 JDK1.7

先来总览一下map的结构:

20200320144054

JDK1.7的源码是在这里复制下来的(不知道为啥下的zip不是源码,还需要编译,懒得折腾了)。

通过查看HashMap的属性,可以看到,也就table(、entrySet)最有可能是HashMap中,实际存储数据的地方:

20200319143007

1.1.1 我们首先来追踪一下构造器:

可以看到,构造器除了基础的参数校验,也只是做了默认的容量设定、阈值因子设定(所谓阈值因子,就是当容量到达一定比例时,就是自动扩容):

20200319165434

1.1.2 然后我们来看一下put方法

现在,我们从put方法来验证一下这个结构:

20200319172849

1.2 JDK1.8

JDK1.8中的HashMap做了比较大的改动,除了性能提升以外,主要对链表也做了优化:如果链表过长,则会被优化成红黑树,但其主体结构与JDK1.7中的HashMap还是比较接近的,本文就暂不展开分析。具体的改动与升级推荐参考:Java 8系列之重新认识HashMap

1.3 HashMap总结

可以看到,基础款的HashMap,是没有对线程安全做任何考虑的,也就是在并发的情况下,我们谨慎使用HashMap,接下来,来看看ConcurrentHashMap是如何做到线程安全的。

二、ConcurrentHashMap

2.1 JDK1.7

在JDK1.7中,ConcurrentHashMap的结构可以说与HashMap的结构有天囊之别,其结构大致如下:可以看到,ConcurrentHashMap内部有多个segment对象,而segment对象中的HashEntity数组才是实际存储数据的地方,ConcurrentHashMap对象中segment的数量,即为当前map锁支持的最大并发数量。

20200320145239

2.1.1 构造器

同样也是从构造器开始追踪,看看在new的时候,我们做了哪些事:

20200320141921

2.1.2 put方法

接下来就是最为关键的put方法了,Segment对象中的table数组也是在put方法中的rehash方法自动扩容的:可以看到,在操作segment对象中的table(HashEntity数组)时,会包裹在lock(tryLock)、unlock方法中,这就实现了线程安全,所以说ConcurrentHashMap对象中segment的数量,即为当前map锁支持的最大并发数量。

20200320110603

2.2 JDK1.8

在jdk1.7中,可以看到ConcurrentHashMap与HashMap的结构完全不同,而在JDK1.8中,ConcurrentHashMap被修改成了与HashMap非常相似的结构:

20200320212439

这里我们就重点梳理一下JDK1.8中ConcurrentHashMap的put方法是如何实现线程安全的:可以看到,在新建node的时候,只是使用了CAS机制进行的无锁操作,而在操作node对象指向的链表/树时(链表达到一定长度会被优化成树形结构),才会使用synchronized锁。而且与HashMap不同的是,新的Node对象是挂在在链表的尾部。

20200320211510

2.3 ConcurrentHashMap总结

可以看到,无论是JDK1.7还是1.8,虽然使用了完全不同的机制,但做了线程安全的考量,当然这带来的性能下降也在情理之中。我们现在了解到了ConcurrentHashMap的内部结构,在初始化的时候,不止是直接new一个出来就万事大吉,而是可以尝试根据业务需要,去是指定一些初始化参数,以避免ConcurrentHashMap的频繁自动扩容。

猜你喜欢

转载自juejin.im/post/5e74c973e51d4526e32c5234