HashMap
hashhMap本质是数组+链表。根据key取得hash值,然后计算出数组下标,如果多个key对应到同一个下标,就用链表连接起来,最新插入的数据在链表头。
HashMap继承AbstractHashMap,实现了Map,Cloneable,Serializable接口
HashMap设计线程不安全的。
对键值对的描述,Node:
|
|
hashmap中键值对的存储形式为链表节点,hasCode相同的节点位于一个桶中。
|
|
红黑树
|
|
HashMap构造函数
|
|
HashMap的构造方法中的三个重要的参数:
- capacity:容量,表示数组容量大小
- loadFactor:比例,扩容用
- threshold:最多容纳的Entry数=capacity*loadFactory,如果当前元素个数多于这个就要扩容(capacity扩容为2倍)
- static final int TREEIFY_THRESHOLD: JDK1.8 新加,Entry链表最大长度,当桶中节点数目大于该长度时,将链表转成红黑树存储;
- static final int UNTREEIFY_THRESHOLD:JDK1.8 新加,当桶中节点数小于该长度,将红黑树转为链表存储;
- static final int DEFAULT_INITIAL_CAPACITY: 默认键值队个数为16
get方法
|
|
先查找对应桶的首元素,然后根据红黑树结构OR链表结构对应查找。
put方法
|
|
散列分布策略:键值对的槽位 = (容量-1) * hash(key)
- 键值对槽位是键值对在tab数组的索引,本质为其hash值对容量取余,但是位运算更快。1234static final int hash(Object key) {int h;return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);}
- 键值对槽位是键值对在tab数组的索引,本质为其hash值对容量取余,但是位运算更快。
HashMap更新和新增的核心思想:
- 根据键值对key的hashCode计算键值对的HashMap中的槽位
- 判断是否空桶或者发生Hash冲突
- 解决hash冲突:根据桶组织形式是红黑树或者链表进行对应插入操作,链表插入操作完成之后,检查是否超过链表阈值,超过将链表转换成红黑树
- 最后检查键值对总数是否超过阈值,超过阈值调用resize()进行rehash操作。
resize方法
|
|
- 当键值对总数超过阈值时,HashMap通过resize方法实现重散列rehash,容量增加为原来的2倍。
remove方法
|
|
ConcurrentHashMap
- 在HashMap的基础上,ConcurrentHashMap放弃了分段锁机制,利用CAS+Synchronized来保证并发更新的安全,低层依然采用数组+链表+红黑树的存储结构
重要属性
|
|
table:默认为null,懒加载,初始化发生在第一次插入的时候,默认大小16,大小总为2的幂次方,用来存储节点数据,可以由迭代器直接调用。
|
|
nextTable:默认为null,扩容时新生成的数组,其大小为原数组的两倍。
|
|
sizeCtl:默认为0,用来控制table初始化和扩容操作,-1表示table正在初始化,-N表示有N-1个线程正在进行扩容操作。如果table未被初始化,表示table需要初始化的大小,如果table初始化完成,表示table的容量,默认为0.75
|
|
Node:保存key,value以及key的hash值的数据结构,val和next由violate修饰,保证并发可见性。
|
|
ForwardingNode:特殊的Node节点,hash值为-1,其中存储nextTable的引用,只有table发生扩容时才有用,作为一个占位符放在table中表示当前节点为null或者已经被移动。
初始化
|
|
会自动调整输入的证书为2的幂次方。
|
|
由于初始化在第一次插入数据时进行,则要确保只有一次初始化:
|
|
sizeCtl默认值为0,如果ConcurrentHashMap实例化时有传参数,sizeCtl会是一个2的幂次方的值。所以和自行第一次put操作的线程会执行compareAndSwapInt方法修改sizeCtl值为-1,有且只有一个线程能够修改成功,其他线程通过Thread.yield()让出CPU时间片等待table初始化完成。
put方法
|
|
put采用CAS+synchronized实现并发插入或更新操作。
- hash算法获取key的hash值
- table中定位索引的位置,n为table的大小
- 获取table中对应索引的元素f,Unsafe.getObjectVolatile来确保获取的数据为最新的。
- f为空,说明该位置第一次插入节点,用Unsafe.compareAndSwapObject插入Node节点。如果CAS成功,说明Node节点已经插入,随后检查当前容量是否需要扩容。如果CAS失败,说明有其他线程提前插入了节点,自旋重新尝试在这个位置插入节点。
- 如果f的值为-1,说明f是当前ForwardingNode节点,意味着有其他线程进行扩容,则一起进行扩容操作。
- 其余情况把新的Node节点按照链或者红黑树的方式插入到合适的位置,这个过程采用同步内置锁实现。
table扩容
|
|
当table容量不足时,及table的元素数量达到容量阈值sizeCtl,需要经table扩容:
- 构建一个nextTable,大小为table的两倍
- 通过Unsafe.compareAndSwapInt修改sizeCtl的值,保证只有一个线程能够输出化nextTable,扩容后数组长度为原来的两倍,但是容量是原来的1.5。
- 把table的数据复制到nextTable中
- 首先根据运算得到遍历的次数i,然后利用tabAt方法获得i位置的元素f,初始化一个forwardNode实例fwd。
- 如果f == null,则在table的i位置放入fwd。
- 如果f是链表的头节点,就构造一个反序链表,把他们分别放在nextTable的i和i+n的位置上,移动完成,采用Unsafe.putObjectVolatile方法给table原位置赋值fwd。
- 如果f是TreeBin节点,也做一个反序处理,并判断是否需要untreeify,把处理的结果分别放在nextTable的i和i+n的位置上,移动完成,同样采用Unsafe.putObjectVolatile方法给table原位置赋值fwd
get方法
|
|
- ConcurrentHashMap是一个并发散列表的实现,它允许完全并发的读取,并且支持给定数量的并发更新。相比于HashTable和同步包装的HashMap,适用一个全局的锁来同步不同线程的并发访问,同一时间点,只能有一个线程持有锁,也就是说在同一个时间点,只有一个线程能访问容器,这虽然保证了多线程的并发访问安全,但是同时也和导致对容器的访问变成串行的了。1.6中采用ReentrantLock分段锁的方式,使用多个线程在不同的segment上进行写操作不会发生阻塞行为。1.8中采用内置锁synchronized。