ConcurrentHashMap和HashMap的区别
1. hashMap概述
HashMap是基于哈希表实现的,每一个元素是一个key-value对,其内部通过单链表解决冲突问题,容量不足(超过了阀值)时,同样会自动增长。
其次,HashMap是非线程安全的,只是用于单线程环境下,多线程环境下应该采用concurrent并发包下的concurrentHashMap。
HashMap 实现了Serializable接口,因此支持序列化,实现了Cloneable接口,能被克隆。
HashMap中key和value都允许为null。key为null的键值对永远都放在以table[0]为头结点的链表中。
1.7及以前的HashMap结构
1.8 后的HashMap结构
2. ConcurrentHashMap概述
HashMap在put的时候,插入的元素超过了容量(由负载因子决定)的范围就会触发扩容操作,就是rehash,这个会重新将原数组的内容重新hash到新的扩容数组中,在多线程的环境下,存在同时其他的元素也在进行put操作,如果hash值相同,可能出现同时在同一数组下用链表表示,造成闭环,导致在get时会出现死循环,所以HashMap是线程不安全的。
HashMap虽然处理数据的效率高,但是在多线程的情况下存在着安全问题,因此设计了CurrentHashMap来解决多线程安全问题。
1.7及以前的ConcurrentHashMap结构
在JDK1.7版本中,ConcurrentHashMap的数据结构是由一个Segment数组和多个HashEntry组成,主要实现原理是实现了锁分离的思路解决了多线程的安全问题,如下图所示:
Segment数组的意义就是将一个大的table分割成多个小的table来进行加锁,也就是上面的提到的锁分离技术,而每一个Segment元素存储的是HashEntry数组+链表,这个和HashMap的数据存储结构一样。
此时的 ConcurrentHashMap 与HashMap和Hashtable 最大的不同在于:
put和 get 两次Hash到达指定的HashEntry,第一次hash到达Segment,第二次到达Segment里面的Entry,然后在遍历entry链表.
put操作
static class Segment<K,V> extends ReentrantLock implements Serializable {
private static final long serialVersionUID = 2249069246763182397L;
final float loadFactor;
Segment(float lf) { this.loadFactor = lf; }
}
从上Segment的继承体系可以看出, Segment实现了ReentrantLock,也就带有锁的功能。
当执行put操作时,会进行第一次key的hash来定位Segment的位置,如果该Segment还没有初始化,即通过CAS操作进行赋值,然后进行第二次hash操作,找到相应的HashEntry的位置,这里会利用继承过来的锁的特性,在将数据插入指定的HashEntry位置时(链表的尾端),会通过继承ReentrantLock的tryLock()方法尝试去获取锁,如果获取成功就直接插入相应的位置,如果已经有线程获取该Segment的锁,那当前线程会以自旋的方式去继续的调用tryLock()方法去获取锁,超过指定次数就挂起,等待唤醒。
get操作
ConcurrentHashMap的get操作跟HashMap类似,只是ConcurrentHashMap第一次需要经过一次hash定位到Segment的位置,然后再hash定位到指定的HashEntry,遍历该HashEntry下的链表进行对比,成功就返回,不成功就返回null
1.8 的ConcurrentHashMap结构
JDK1.8的实现已经摒弃了Segment的概念,而是直接用Node数组+链表+红黑树的数据结构来实现,并发控制使用Synchronized和CAS来操作,整个看起来就像是优化过且线程安全的HashMap,虽然在JDK1.8中还能看到Segment的数据结构,但是已经简化了属性,只是为了兼容旧版本.
总结区别点:
HashMap
线程不安全。ConcurrentHashMap
线程安全。ConcurrentHashMap
在Java7
中使用segment
实现,对每个segment
加锁。
ap`线程安全。ConcurrentHashMap
在Java7
中使用segment
实现,对每个segment
加锁。ConcurrentHashMap
在Java8
中是直接在HashMap
的基础上进行加锁。