Java集合HashMap不安全后果及ConcurrentHashMap 原理

HashMap

Map是我们在集合中非常重要的一个集合、我们刚学习HashMap的时候就说它不安全、可是不知道不安全会发生什么后果

我们先来看看JDK7和JDK8当中的HashMap有什么不一样

JDK7 JDK8
数据结构 数组+ 链表。复杂度:O(n) 数组 + 链表 + 红黑树
插入位置 头插法 尾插法

JDK7 HashMap链表循环造成死循环

造成HasMap链表循环列表的原因就是因为在hash冲突的时候采用了头插法且没有加锁的方式插入链表、在HashMap put的时候,put函数会检查元素是否超出了阈值【数组的总的添加元素数大于了 数组长度 * 0.75(默认,也可自己设定)】,如果超出了数组长度扩容为两倍,下面是它扩容时将旧hash表转到新hash表从而完成扩容的源代码

 /**
* 作用:将旧数组上的数据(键值对)转移到新table中,从而完成扩容
* 过程:按旧链表的正序遍历链表、在新链表的头部依次插入。但是这样会导致扩容完成后,链表逆序
*/
void transfer(Entry[] newTable, boolean rehash) {
int newCapacity = newTable.length;
//通过遍历 旧数组,将旧数组上的数据(键值对)转移到新数组中
for (Entry<K,V> e : table) {
// 遍历hash碰撞形成的链表
while(null != e) {
// 拿到当前头节点的下一个节点
Entry<K,V> next = e.next;
// 是否要重新计算hashCode
if (rehash) {
e.hash = null == e.key ? 0 : hash(e.key);
}
//通过hashCode计算出hash表的槽
int i = indexFor(e.hash, newCapacity);
// 新hash表的hash槽赋值给e 的下一个节点
e.next = newTable[i];
// 讲当前元素,赋给新数组的对应下标位置。
newTable[i] = e;
// 访问下1个Entry链上的元素,如此不断循环,直到遍历完该链表上的所有节点
e = next;
}
}
}

在单线程中,这样的代码是没有什么问题,问题就是有很多个线程,我们假设有两个线程t1和t2,他们都执行到Entry<K,V> next = e.next;这一行,此时图示:

,t1的时间片已经用完了,t2还在继续执行,此时t1、t2获取的e和next分别为e1、e2、next1、next2,再之后t2完成了扩容操作,链表顺序已经从原来的abcd变成了dcba,t1线程的e在next的上方,如下图示:

此时t1线程醒过来了,继续执行就会出现链表循环,造成while死循环

JDK8 HashMap中已经采用尾插法进行插入避免了链表循环、且链表长度大于8会变成红黑树

HashMap数据丢失

jdk7:hashMap put的时候有hash冲突如果没有超过阈值,就会采用头插法来插入链表,假如有t1和t2线程,它们都同时获得了,链表的头节点,此时t1线程的时间片没有了,t2线程还在继续,t2线程已经执行完了put操作,t1线程醒过来,t1线程会将自己的下一个节点指向头节点,这样刚刚t2线程put的节点就丢失了

jdk8: 采用的是尾插法、一样的两个线程都同时获取了尾节点、后执行的那个线程会覆盖掉前一个线程的节点、造成丢失

HashMap在官方的设定的就是线程不安全的,要安全选ConcurrentHashMap

JDK7 ConcurrentHashMap

在jdk7 ConcurrentHashMap当中采用了一个分段锁的方式(Segment[])来保证线程安全

Segment类继承了ReentrantLock,Segment类里有多个槽。

put:

计算出key在那个Segment,然后上锁,再算出在哪个槽里,此时如果有其它线程访问这个Segment会被阻塞住,直到unlock。

get:

CAS + volatile

在HashTable当中是锁住了整个对象,线程t1 get("key1") 、线程t2 put("key2", "value2"),线程t2也会被阻塞住,在ConcurrentHashMap中这两个操作是不会阻塞且不受影响。

值得注意的是Segment[]的初始长度,默认是16,不会因为数据的多少而改变,所以默认最多只有16个线程获得锁,这个初始值可以设置,但是需要我们自己去评估多少合适。

Segment分段锁实现下的ConcurrentHashMap的劣势:

  • 寻找元素需要经过两次hash
  • 并发级别(Segment[]长度)的合理设置问题。虽然提供了默认的并发级别,但是是否在大多数场景下都适用?归根结底,就是需要用户自己来评估需要多少把锁来分段治理的问题。
  • 在存储成本比HashMap高。每个Segment管理的最少容量是2,而默认的并发级别为16,即16个Segment。假如我只需要存储的是16个元素,那么ConcurrentHashMap会需要占用2倍的存储空间。

JDK8 ConcurrentHashMap

在JDK8中,ConcurrentHashMap采用更加细粒度的锁取消分段锁,synchronized + CAS

put:

如果发现该槽没有数据,初始化头节点时,ConcurrentHashMap并没有加锁,而是CAS的方式进行原子替换(原子操作,基于Unsafe类的原子操作API)

如果发现该槽有数据,判断是否正在扩容,如果是则会去帮助扩容,扩容时会进行加锁处理,锁定是头节点。

如果发现该槽有节点且不在扩容,则会去锁(synchronized)住该槽的头节点,进行插入

get:

没有什么加锁的操作,hash表被volatile修饰

HashMap不安全后果及ConcurrentHashMap线程安全原理的更多相关文章

  1. HashMap、HashTable 和 ConcurrentHashMap 线程安全问题

    一.HashMap HashMap 是线程不安全的. JDK 1.7 HashMap 采用数组 + 链表的数据结构,多线程背景下,在数组扩容的时候,存在 Entry 链死循环和数据丢失问题. JDK ...

  2. 面试必问之 ConcurrentHashMap 线程安全的具体实现方式

    作者:炸鸡可乐 原文出处:www.pzblog.cn 一.摘要 在之前的集合文章中,我们了解到 HashMap 在多线程环境下操作可能会导致程序死循环的线上故障! 既然在多线程环境下不能使用 Hash ...

  3. 【学习】005 线程池原理分析&锁的深度化

    线程池 什么是线程池 Java中的线程池是运用场景最多的并发框架,几乎所有需要异步或并发执行任务的程序 都可以使用线程池.在开发过程中,合理地使用线程池能够带来3个好处. 第一:降低资源消耗.通过重复 ...

  4. ConcurrentHashMap 的实现原理

    概述 我们在之前的博文中了解到关于 HashMap 和 Hashtable 这两种集合.其中 HashMap 是非线程安全的,当我们只有一个线程在使用 HashMap 的时候,自然不会有问题,但如果涉 ...

  5. java并发包&线程池原理分析&锁的深度化

          java并发包&线程池原理分析&锁的深度化 并发包 同步容器类 Vector与ArrayList区别 1.ArrayList是最常用的List实现类,内部是通过数组实现的, ...

  6. ConcurrentHashMap 的工作原理及代码实现

    ConcurrentHashMap采用了非常精妙的"分段锁"策略,ConcurrentHashMap的主干是个Segment数组.Segment继承了ReentrantLock,所 ...

  7. jdk1.8 ConcurrentHashMap 的工作原理及代码实现,如何统计所有的元素个数

    ConcurrentHashMap 的工作原理及代码实现: 相比于1.7版本,它做了两个改进 1.取消了segment分段设计,直接使用Node数组来保存数据,并且采用Node数组元素作为锁来实现每一 ...

  8. java多线程系类:JUC线程池:03之线程池原理(二)(转)

    概要 在前面一章"Java多线程系列--"JUC线程池"02之 线程池原理(一)"中介绍了线程池的数据结构,本章会通过分析线程池的源码,对线程池进行说明.内容包 ...

  9. Java多线程系列--“JUC线程池”03之 线程池原理(二)

    概要 在前面一章"Java多线程系列--“JUC线程池”02之 线程池原理(一)"中介绍了线程池的数据结构,本章会通过分析线程池的源码,对线程池进行说明.内容包括:线程池示例参考代 ...

随机推荐

  1. Json多层级动态结构数据解析

    一.工具 (1)GSON Google Gson是一个简单的基于Java的库,用于将Java对象序列化为JSON,反之亦然. 它是由Google开发的一个开源库. 以下几点说明为什么应该使用这个库 - ...

  2. dotnet 控制台 使用 Microsoft.Maui.Graphics 配合 Skia 进行绘图入门

    本文将告诉大家如何在 dotnet 的控制台模式下,采用 MAUI 自绘库 Microsoft.Maui.Graphics 进行绘图,设置 Microsoft.Maui.Graphics 底层调用 M ...

  3. Min-max 容斥与 kth 容斥

    期望的线性性: \[E(x+y)=E(x)+E(y) \] 证明: \[E(x+y)=\sum_i \sum_j(i+j)*P(i=x,j=y) \] \[=\sum_i\sum_ji*P(i=x,j ...

  4. day11 - 多线程

    1内容 进程.线程介绍 Java中 线程的实现方式 Thread 类 Runnable 接口 Callable 接口 线程相关的方法 线程安全问题 - 同步技术 线程等待唤醒机制 进程(Process ...

  5. 静态static关键字修饰成员方法和静态static的内存图

    当 static 修饰成员方法时,该方法称为类方法 .静态方法在声明中有 static ,建议使用类名来调用,而不需要 创建类的对象.调用方式非常简单 ~类方法:使用 static关键字修饰的成员方法 ...

  6. mvc url重写

    public class newDomainRoute : Route { private Regex domainRegex; private Regex pathRegex; public str ...

  7. Hadoop学习 Hadoop-HA 解释和概念介绍

    一.Hadoop-HA 1.1 Hadoop1.x带来的问题 1.单点故障 a. 每个群集只有一个NameNode,NameNode存在单点故障(SPOF). ​ b. 如果该计算机或进程不可用,则整 ...

  8. zabbix监控添加学习笔记

    在实际生产环境中,除了CPU.内存等一些系统信息可以挂载zabbix的自带模板Template OS Linux:但是一些公司开发的定制服务需要自己写模板或者监控项去监控: 一.监控公司的java服务 ...

  9. DML添加数据&删除数据&修改数据

    DML:增删改表中数据 1.添加数据: 语法:insert into 表名(列名1,列名2).... values(值1,值2): 注意: 1.列名和值要一一对应. 2.如果表名后,不定义列名,则默认 ...

  10. python不同平台进程的启动与终止

    Liunx进程的启动与终止 在使用subprocess创建进程时需要将所有进程设置为一个进程组 preexec_fn:只在 Unix 平台下有效,用于指定一个可执行对象(callable object ...