HashMap和ConcurrentHashMap
关于HashMap和ConcurrentHashMap的一些记录
HashMap
HashMap基本结构是数组+单向链表/红黑树。
map有几个比较重要的变量
- DEFAULT_INITIAL_CAPACITY 默认初始容量,16
- DEFAULT_LOAD_FACTOR 默认负载因子0.75,用在扩容上
- TREEIFY_THRESHOLD 转换成红黑树的链表长度 8
- UNTREEIFY_THRESHOLD 由树转换成链表的长度,6
- MIN_TREEIFY_CAPACITY 转换成红黑树的数组的最小长度 64,如果某链表长度大于8,但是此时数组长度不够,则只会扩容,不会转换
贴出openjdk11里的put方法
1 | |
再看扩容算法
1 | |
此外还有一些细节
初始容量不为2的n次幂的,会向上调整为最近的2次幂,同时由于扩容都是翻两倍,所以容量始终的2的n次幂
扩容的阈值=容量负载因子。只要当前数据量大于这个值就会触发扩容。假设现在有1000个数据,容量应该是2048,因为10240.75=768触发扩容,但是这样会浪费很多空间,可以通过吧负载因子设置为1来避免,
HashMap线程不安全体现在会造成死循环、数据丢失、数据覆盖这些问题。其中死循环和数据丢失是在JDK1.7中出现的问题,在JDK1.8中已经得到解决,然而1.8中仍会有数据覆盖这样的问题
在扩容时,先将数组设置为两倍大小的空数组,这时线程挂起,同时其他线程插入数据,再回来继续散列,数据可能就被覆盖了
ConcurrentHashMap
1.7 的并发安全是通过Segment分段加锁实现的。1.8则使用了CAS+synchronized来实现
以下是openjdk11里的代码
直接看put方法
1 | |
基本遵循以下逻辑
- 计算hash,开始循环
- 如果现在没有数据,则初始化一个table
- 如果此时对应位置上没有数据,那么就尝试cas设置新值。设置不成功则开始自旋设置,直到当前线程设置成功,或者别的线程设置成功,则当前线程判断有值,不再进行cas
- 有个onlyIfAbsent暂时不知道干嘛的,但是一般这个参数都为false,相应分支不会执行
- 当当前位置有值,则开始追加数据,此时对这个节点加锁。这样加锁粒度比HashTable加在整个对象上要更小
- 循环结束后,增加总数。这边也是CAS+自旋逻辑
ConcurrentHashMap中有个非常重要的变量sizeCtl
1 | |
当这个值为负数时,代表正在进行初始化或者扩容操作。-1代表初始化,-(1+参与扩容的线程数)代表正在扩容
初始化
初始化操作仅允许一个线程进行。在方法initTable中,如果发现sizeCtl是-1,则使用Thread.yield()让出cpu时间。注意:这种让渡是提示性的,而非强制,所以此处可能也会进行自旋等待初始化线程结束初始化,当前线程直接判断table不为null
总结:如果节点位置没有值,就用cas设置;有值,就对节点加锁。抛弃了Segment,但是相关的类还保留在源代码里,不知道为什么不删