1.为什么HashMap不安全?
JDK1.7有环(1.8从头插法改为了尾插法),put数据的时候没有考虑多线程,可能会造成数据覆盖;(环是什么?1.7在并发情况下,扩容迁移数据时形成环,造成CPU资源占用飙高)
计数器也是传统的++,在记录元素个数和HashMap写的次数时,记录不准确。
数据迁移,也可能会丢失数据
2. ConcurrentHashMap如何保证线程安全?
尾插,其次扩容有CAS保证线程安全。
写入数组时,基于CAS保证安全,挂入链表或插入红黑树时,基于synchronized(锁的当前索引)保证安全;
使用的LongAdder实现的技术,底层还是CAS,(类似是AtomicLong)
ConcurrenHashMap扩容时,一点基于CAS保证数据迁移不出现并发问题,其次ConcurrentHashMap还提供了并发扩容的操作,如两个线程同时扩容分别领取不同索引范围的迁移任务,关键是领取任务是如何保证线程安全。
3.ConcurrentHashMap构建好,数组就创建出来了吗,如果不是如何保证线程安全?
ConcurrentHashMap是懒加载机制,并不是构建好就创建了数组,而是有数据了才会创建数组;
基于CAS来保证初始化线程安全的,采用CAS去修改sizeCtl的变量去控制线程初始化数据的原子性,同时还采用DCL(双重检查机制,类似单例模式的DCL),外层判断数组未初始化,中间基于CAS修改sizeCtl,内层再做数组未初始化判断;
代码含义:
1.外层判断数组是否初始化
2.if((sc = sizeCtl) < 0)判断sizeCtl是否已改为-1,CAS若已改为-1代表已经在有线程初始化,此时当前线程可以让出CPU;
3.CAS修改sizeCtl值为-1,有且只有一个线程能修改成功
4.再次判断数组是否初始化,未初始则开始初始化;

4.为什么负载因子是0.75,为什么链表长度达到8转为红黑树?
ConcurrenHashMap的负载因子不允许修改。
0.5,如果负载因子是0.5,数据添加一半就开始扩容了,空间利用率低
1,如果是1,hash碰撞特别频繁,数据挂到链表,影响生成红黑树和写入红黑树的效率;
0.75是剧中的选择,两方面都兼顾了
再深入就是泊松分布原理,源码注释中通过泊松分布得出,链表达到8的概率非常低,是0.00000006,生成红黑树的概率特别低;
虽然ConcurrentHashMap引入了红黑树,但是红黑树对于写入的维护成本比较高,查询的时间复杂度是O((logn));所以尽可能规避使用红黑树;
5.put操作太频繁的场景,会造成扩容时间put阻塞吗?
一般情况下不会造成阻塞。
因为如果在put操作时,发现当前索引位置并没有数据时,正常把数据落到老数组;
如果put操作时,发现当前位置数据已经被迁移到了新数组,这时无法正常插入,会先去帮助扩容,快速结束扩容,并且从新选择索引位置插入;
如代码图;while循环中,判断状态为迁移时,帮助扩容,直到结束扩容,重新插入数据;

6.ConcurrenHashMap何时扩容,扩容流程?
ConcurrentHashMap中的元素个数,达到了负载因子计算的阈值,那么直接扩容
当调用putAll大量数据时,也可能会造成直接扩容操作,大量的数据时超过了扩容的阈值直接扩容,然后再插入
数组长度小于64,并且链表长度大于等于8时,扩容;
扩容的流程:
每个扩容的线程都需要基于oldTable的长度计算一个扩容表示戳(避免出现两个线程扩容的数组长度不一致,其次保证扩容表示戳的16位是1,这样左移16位会得到一个负数,因为sizeCtrl 小于1代表正在扩容)
第一个扩容的线程,会对sizeCtl的低位 +2 ,代表当前有1个线程来扩容
除去第一个扩容的线程,其他线程会对siezCtl +1,代表现在又来了一个线程帮助扩容
第一个线程会初始化新数组,每个线程会领取迁移数据的任务,将oldTable中的数据迁移到新数组。默认情况下,每个线程每次领取长度为16的迁移数据任务;(若只有一个线程扩容,会多次领取迁移任务)
当数据迁移完毕时,每个线程再去领取任务时,发现没任务可领了,推出扩容,对sizeCtl - 1;
最后一个退出扩容的线程,发现 -1之后还剩1,最后一个退出扩容的线程会从头到尾再检查一次,有没有遗留的数据没有迁移走;(这种情况基本不会发生),检查完之后,再 -1,这样siezCtl扣除完,扩容结束;
7.ConcurrentHashMap得计数器如何实现的?
这里是基于LongAdder的机制实现,但是并没有直接用LongAdder的引用,而是根据LongAdder的原理写了一个相似的代码。LongAdder使用CAS添加,保证原子性,其次基于分段锁,保证并发性;
8. ConcurrentHashMap 的读操作会阻塞吗?
无论查哪,都不阻塞。
查询数组?:第一块就是查看元素是否在数组,在就直接返回。
查询链表?:第二块,如果没特殊情况,就在链表next查询返回即可。
扩容时?:第三块,如果当前索引位置是-1,代表当前位置数据全部都迁移到了新数组,直接去新数组查询,不管有没有扩容完。
查询红黑树?:如果有一个线程正在写入红黑树,此时读线程还能去红黑树查询吗?因为红黑树为了保证平衡可能会旋转,旋转会换指针,可能会出现问题,所以在转会红黑树时,不但有一个红黑树,还会保留一个双向链表,此时会查询双向链表,不让读线程阻塞。至于如何判断是否有线程在写,和等待写或者读红黑树,根据TreeBin方法的的lockState来判断,如果是1,代表有线程正在写,如果是2,代表有写线程等待写,如果是4n,代表有多个线程在做读操作。