注意
本系列文章是笔者在参考了大量优秀文章的基础上的理解,由于水平有限,难免会出现错误,望批评指正,同时我也会把自己最新的理解及时更新到文章中去。最后,我会在文中注明参考文章,特别感谢这些无私奉献的作者。
上一篇文章我们提到 HashMap 是线程不安全的 Map ,那有什么线程安全的 Map 呢?
Java 提供了三种实现线程安全 Map 的方式:
1、HashTable
2、Collections.synchronizedMap()
3、ConcurrentHashMap
对于 HashTable ,它是 Java 1.0 版本的时候推出的 map 类,比 HashMap 要推出的早。HashTable为了线程安全,几乎是在每个方法前面都加上了 synchronized 关键字,这和 List 中的 Vector 及其类似。
对比 HashTable 和 HashMap ,主要以下几个区别:
1、HashTable 在每个操作方法加了 synchronized 关键字,而 HashMap 没有同步措施。
2、由于加了同步策略,HashTable 的执行效率很低。
3、HashTable 在扩容时,扩大到原来的 2 倍加 1,而 HashMap 则是扩大到原来的 2 倍。
4、HashTable 继承了 Dictionary,HashMap 继承了 AbstractMap。
5、HashTable 没有区分 1.7 和 1.8 版本没有什么区别,都是数组+链表结构。
第二种方式 Collections.synchronizedMap(),只是对 Map 做了一层 synchronized 包装,同样是执行效率低。
第三种方式是 ConcurrentHashMap ,分为 JDK 1.7 版本和 JDK 1.8 版本(之后的版本笔者没有研究过),直接看下面链接中的文章吧(笔者对于扩容和数据迁移没有完全理解)。
我就知道面试官接下来要问我 ConcurrentHashMap 底层原理了
笔者在看以上链接文章 JDK 1.8 部分时,还结合了一个视频来看,这是视频地址,也是没有完全看懂。
下面对链文中笔者能够理解的知识总结一下:
1、JDK 1.7 中的 ConcurrentHashMap 采用分段锁的方式对 HashMap 加锁,它内部维护着一个 Segment 数组,每个 Segment 内维护着一个存放键值对的数组,Segment 就和小型 HashMap 一样,同样有扩容、数据迁移等操作。ConcurrentHashMap 的 size 计算方式有很大的不同,它是所有 Segment 中数组元素个数的总和,求和过程是:
首先尝试用 CAS 的方式求值,如果过程中有其它线程更改了 ConcurrentHashMap (通过 modCount 就能发现),则这一轮的求值失败,反复尝试多次如果不成功就把每个 Segment 都加锁,再分别获取求和。
2、JDK 1.8 中的 ConcurrentHashMap 中采用的 CAS + Synchronized 实现线程安全。
当添加的元素时,如果数组为空,则通过CAS自旋新建数组。数组不为空,但添加位为空时,直接用CAS添加。当添加位key的hash==MOVE时,表示 ConcurrentHashMap 正在扩容,则帮助其扩容。以上条件都不满足的话,就进入Synchronized代码块执行元素操作(类似 JDK 1.8 的 HashMap)。最后执行 addCount 方法将元素个数 +1 并执行扩容判断逻辑。
元素个数+1,涉及操作 CounterCell[] 和 baseCount,试想为了 size 值正确我们完全可以将 size 设置成 Volatile 类型,然后用 CAS 自旋来实现size的加1操作,但如果有很多线程同时对size做加1操作的话,效率就不高了,所以 JDK 1.8 的 ConcurrentHashMap 通过 baseCount 和 CounterCell数组两个属性同时参与元素个数的统计,具体就是通过 CAS 操作保证最后baseCount加1成功或者 CounterCell 数组中的某个对应 CounterCell 加1成功。
扩容大概流程是从数组最后一位往前遍历,确定每个线程负责转移的步长(即数组区域),每个转移完成的桶位置都放置一个 ForwardingNode ,直到转移完成。
3、ConcurrentHashMap 不论是 JDK 1.7 还是 JDK 1.8扩容后的新哈希表中对应节点都会重新新建对象,这一点和 HashMap 不一样,HashMap 还是原来的节点。
一个问题:HashTable, ConcurrentHashMap为什么键和值不能为null,而 HashMap 可以 ?
因为 HashTable , ConcurrentHashMap 它们是用于多线程的,并发的 ,如果map.get(key)得到了null,不能判断到底是映射的value是null,还是因为没有找到对应的key而为空,而对于单线程状态的HashMap 却可以用 containKey(key) 去判断到底是否包含了这个key。比如:一个线程先get(key)再containKey(key),这两个方法的中间时刻,其他线程怎么操作这个key都会可能发生,例如删掉这个key,所以多线程场景中 value 为 null 就很容易产生歧义。
至于,为什么 key 也不能为 null ,我想不出这是为什么。