在分析ConcurrentHashMap之前,先来看看基础款的HashMap源码是什么样的
一、HashMap
1.1 JDK1.7
先来总览一下map的结构:
JDK1.7的源码是在这里复制下来的(不知道为啥下的zip不是源码,还需要编译,懒得折腾了)。
通过查看HashMap的属性,可以看到,也就table(、entrySet)最有可能是HashMap中,实际存储数据的地方:
1.1.1 我们首先来追踪一下构造器:
可以看到,构造器除了基础的参数校验,也只是做了默认的容量设定、阈值因子设定(所谓阈值因子,就是当容量到达一定比例时,就是自动扩容):
1.1.2 然后我们来看一下put方法
现在,我们从put方法来验证一下这个结构:
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锁支持的最大并发数量。
2.1.1 构造器
同样也是从构造器开始追踪,看看在new的时候,我们做了哪些事:
2.1.2 put方法
接下来就是最为关键的put
方法了,Segment
对象中的table
数组也是在put
方法中的rehash
方法自动扩容的:可以看到,在操作segment对象中的table(HashEntity数组)时,会包裹在lock(tryLock)、unlock方法中,这就实现了线程安全,所以说ConcurrentHashMap对象中segment的数量,即为当前map锁支持的最大并发数量。
2.2 JDK1.8
在jdk1.7中,可以看到ConcurrentHashMap与HashMap的结构完全不同,而在JDK1.8中,ConcurrentHashMap被修改成了与HashMap非常相似的结构:
这里我们就重点梳理一下JDK1.8中ConcurrentHashMap的put方法是如何实现线程安全的:可以看到,在新建node的时候,只是使用了CAS机制进行的无锁操作,而在操作node对象指向的链表/树时(链表达到一定长度会被优化成树形结构),才会使用synchronized锁。而且与HashMap不同的是,新的Node对象是挂在在链表的尾部。
2.3 ConcurrentHashMap总结
可以看到,无论是JDK1.7还是1.8,虽然使用了完全不同的机制,但做了线程安全的考量,当然这带来的性能下降也在情理之中。我们现在了解到了ConcurrentHashMap的内部结构,在初始化的时候,不止是直接new一个出来就万事大吉,而是可以尝试根据业务需要,去是指定一些初始化参数,以避免ConcurrentHashMap的频繁自动扩容。