HashMap之非线程安全

HashMap非线程安全

学习笔记,搜罗了多个文档的总结。

1. 为什么是非线程安全的?

1)Rehash后形成循环链表
HashMap添加元素发生冲突时,采用拉链法解决,也就是将相同hashcode的两个key存放到数组中同一位置下的链表中。当该链表的长度过大时,会进行ReSize操作,之后会将原数组中的元素重新散列到新数组中,即ReHash操作。【注:ReSize的时机:HashMap默认大小16,loadFactory=0.75,即第一次扩容发生在当数组长度大于 16 * 0.75 = 12 时。】
JDK1.7中,扩容之后链表中元素的会发生逆转, 所以会产生循环链表,每个元素的next指针都不为null,造成死循环。此时,会把CPU使用率占到接近100%。
JDK1.8后,解决了这循环链表的个问题,改进之后的方法不再进行链表的逆转, 而是保持原有链表的顺序, 如果在多线程环境下, 顶多会在链表后边多追加几个元素而已, 不会出现环的情况。
2)造成数据丢失
当两个线程同时put时,这两个key的hashcode相同,会发生碰撞,被添加到同一条链表上。那么就会导致一个值被另一个值覆盖,造成数据丢失。

2. 有哪些线程安全的方式?

  1. ConcurrentHashMap(建议使用)
    同 Hashtable 相比,ConcurrentHashMap 不仅保证了访问的线程安全性,而且在效率上有较大的提高。ConcurrentHashMap 的扩容只会扩容到原来的两倍。老数组里的数据移动到新的数组时,位置要么不变,要么变为 index+ oldSize,参数里的 node 会在扩容之后使用链表头插法插入到指定位置。

    JDK1.7 的 ConcurrentHashMap 底层采用 数组(分段segment数组 + HashEntry数组)+ 链表 实现。Segment 数组中的每个元素包含一个 HashEntry 数组,每个 HashEntry 数组属于多个链表结构。segment的个数一旦初始化不能改变。采用拉链法解决冲突。
    JDK1.7时,ConcurrentHashMap进行插入和读取数据时,根据key定向到对应的Segment,只对这个segment进行锁定,即 分段锁(减小锁的范围),所以JDK1.7的锁粒度是基于Segment的(包含多个HashEntry),每一个 Segment 上同时只有一个线程可以操作。多线程访问容器里不同数据段的数据,就不会存在锁竞争,提高并发访问率。JDK1.7 最大并发度是 Segment 的个数。

    JDK1.8 采用的数据结构跟 HashMap1.8 的结构一样,数组+链表/红黑二叉树。当冲突链表的长度达到一定长度时,链表将会转化为红黑树,以提高搜索效率。采用拉链法+红黑树解决冲突。
    JDK1.8 ConcurrentHashMap 并发控制使用 Node + CAS + synchronized 来保证线程安全。锁粒度更细,锁的粒度就是HashEntry(每个链表的首结点),实现的复杂度降低了。synchronized 只锁定当前链表或红黑二叉树的首节点,这样只要 hash 不冲突,就不会产生并发,就不会影响其他 Node 的读写,效率大幅提升。JDK 1.8 最大并发度是 Node 数组的大小,并发度更大。

  2. Hashtable(少用,最好别用)
    Hashtable 是将绝大部分方法都加上 synchronized 来保证线程安全的,由于绝大部分方法都同步,所以它的性能低下,现在已经被弃用。hashtable底层采用 数组 + 链表 实现。

  3. synchronizedMap
    Sysnchronized Map 和 Hashtable 相似,也是采用 synchronized 来保证线程安全的,唯一不同的是,SysnchronizedMap使用一个 Object 对象进行同步,并且它也没有被没遗弃。

猜你喜欢

转载自blog.csdn.net/weixin_45463572/article/details/130421272
今日推荐