1、jdk1.8的HashMap设计
1.1、数据结构:数组+链表+红黑树(jdk1.6、jdk1.7采用数据+链表)
1.2、链表和红黑树的关系
当数组大小已经超过64并且链表中的元素个数超过默认设定(8个)时,将链表转化为红黑树
1.3、PUT
- 根据key计算出hash值
- hash值&(数组长度-1)得到所在数组的index
- 如果该index位置的Node元素不存在,则直接创建一个新的Node
- 如果该index位置的Node元素是TreeNode类型即红黑树类型了,则直接按照红黑树的插入方式进行插入
- 如果该index位置的Node元素是非TreeNode类型则,则按照链表的形式进行插入操作
- 链表插入操作完成后,判断是否超过阈值TREEIFY_THRESHOLD(默认是8),超过则要么数组扩容要么链表转化成红黑树
- 判断当前总容量是否超出阈值,如果超出则执行扩容
注意:jdk1.8是在链表的尾部追加,jdk1.7、jdk1.6是在链表的头部添加;
1.4、并发问题
HashMap在多线程进行扩容时可能会产生环状链表,get()时造成CPU 100%的问题。
2、jdk1.8的ConcurrentHashMap设计
2.1、数据结构:同HashMap,为高并发而设计的。
2.2、PUT
volatile Node<K, V> [] table;
private volatile int sizeCtl;
- 如果数组还未初始化,通过Unsafe的CAS操作将sizeCtl设置为-1,设置成功者,可以进行初始化操作;
- 根据key的hash值找到对应的node,如果node==null,那么通过一个CAS原子操作来设置桶的第一个元素,失败的则继续执行插入或更新;
- 如果node!=null,但第一个元素的hash值是-1,说明此时该桶正在进行迁移操作(又来其它线程的话,会帮着一起迁移)。 否则获取该node的锁(Synchronized)执行链表(数组大小超过64 & 链表中元素超过8转成红黑树)或者红黑树的插入或更新;
注意:jdk1.7开始大量使用CAS算法+Volatile来提升效率
2.3、SIZE(跨段操作)
jdk1.7实现方式
先尝试无锁的情况下计算最多3次的计算结果,比较两次结果一致就认为当前没有元素加入计算是准确的。否则会给每个segment加上锁,然后计算
jdk1.8实现方式
统计CounterCell[]中每个CounterCell的和,这是一个估计值,牺牲了精度来换取更高的效率。
2.4、并发度(Concurrency Level)比较
jdk1.8为node[]的大小,可变(每次数组扩容,并发度就扩大一倍),锁的是node,力度更细。
jdk1.6、jdk1.7为segment[]的大小,初始化完成之后就不变了,采用的是锁分离(分段锁)。
HashTable锁的是整个表,并发度只有1。多线程竞争时效率很低。
2.5、读为什么不需要锁?
因为对数据的读写是一个原子操作,可以不需要读锁的。但是需要保证能读到最新数据,所以必须加volatile。即数组的引用需要加volatile,同时一个Node节点中的val和next属性也必须要加volatile。
jdk1.6、jdk1.7通过两次hash可以快速找到需要的元素。
jdk1.8通过链表+红黑树的形式弥补了put和get时的性能差距。
jdk1.8为什么用Synchronized?之前版本用的是ReentrantLock可重入锁
AQS是API级别的。后续优化空间很小,Synchronized是JVM直接支持的,有锁粗化、锁消除、锁自旋特性,可以直接随JDK版本升级而提升
2.6、数据一致性
Jdk1.7、1.8是强一致性,写入的数据可以立马读到
Jdk1.6是弱一致性
弱一致性:put操作将一个元素加入到底层数据结构后,get可能在某段时间内还看不到这个元素
ref:https://my.oschina.net/pingpangkuangmo/blog/817973