1.HashMap的底层数据结构?
HashMap底层实现数据结构为数组+链表的形式
JDK8及其以后的版本中使用了数组+链表+红黑树实现,解决了链表太长导致的查询速度变慢的问题。
简单来说,HashMap由数组+链表组成的,数组是HashMap的主体,链表则是主要为了解决哈希冲突而存在的。
HashMap通过key的HashCode经过扰动函数处理过后得到Hash值,然后通过位运算判断当前元素存放的位置,如果当前位置存在元素的话,就判断该元素与要存入的元素的hash值以及key是否相同,如果相同的话,直接覆盖,不相同就通过拉链法解决冲突。当Map中的元素总数超过Entry数组的0.75时,触发扩容操作,为了减少链表长度,元素分配更均匀。
2.HashMap是线程安全的吗?
HashMap 是非线程安全的.
快速失败 (fast-fail)
“ 快速失败 ” 也就是 fail-fast ,它是 Java 集合的一种错误检测机制。当多个线程对集合进行结构上的改变的操作时,有可能会产生 fail-fast 机制。
记住是有可能,而不是一定。
例如:假设存在两个线程(线程 1 、线程 2 ),线程 1 通过 Iterator 在遍历集合 A 中的元素,在某个时候线程 2 修改了集合 A 的结构(是结构上面的修改,而不是简单的修改集合元素的内容),那么这个时候程序就会抛出 ConcurrentModificationException 异常,从而产生 fail-fast 机制。
在 HashMap 的 forEach 方法中有以下代码:
public void forEach ( BiConsumer <? super K , ? super V > action ) {
Node < K , V > [] tab ;
if ( action == null )
throw new NullPointerException ();
if ( size > 0 && ( tab = table ) != null ) {
int mc = modCount ;
for ( int i = 0 ; i < tab . length ; ++ i ) {
for ( Node < K , V > e = tab [ i ]; e != null ; e = e . next )
action . accept ( e . key , e . value );
}
if ( modCount != mc )
throw new ConcurrentModificationException ();
}
}
在上面我们说到, modCount 是记录每次 HashMap 结构修改。 forEach 方法会在在进入 for 循环之前,将 modCount 赋值给 mc ,如果在 for 循环之后, HashMap 的结构变化了,那么导致的结果就是 modCount != mc ,则抛出 ConcurrentModificationException() 异常。
3.多线程可以用哪种Map
3.1 HashTable
HashTable 中所有 CRUD 操作都是线程同步的。
同样的,线程同步的代价就是效率变低了。
public synchronized V get(Object key) {
……
}
public synchronized V put(K key, V value) {
……
}
public synchronized V remove(Object key) {
……
}
.....
3.2 ConcurrentHashMap
ConcurrentHashMap 和 Hashtable 的区别主要体现在实现线程安全的方式上不同。
底层数据结构:
JDK1.7的 ConcurrentHashMap 底层采用 分段的数组+链表 实现,JDK1.8 采用的数据结构跟HashMap1.8的结构一样,数组+链表/红黑二叉树。
Hashtable 和 JDK1.8 之前的 HashMap 的底层数据结构类似都是采用 数组+链表 的形式,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的;
实现线程安全的方式(重要):
① 在JDK1.7的时候,ConcurrentHashMap(分段锁) 对整个桶数组进行了分割分段(Segment),每一把锁只锁容器其中一部分数据,多线程访问容器里不同数据段的数据,就不会存在锁竞争,提高并发访问率。(默认分配16个Segment,比Hashtable效率提高16倍。)
到了 JDK1.8 的时候已经摒弃了Segment的概念,而是直接用 Node 数组+链表+红黑树的数据结构来实现,并发控制使用 synchronized 和 CAS 来操作。(JDK1.6以后 对 synchronized锁做了很多优化) 整个看起来就像是优化过且线程安全的 HashMap,虽然在JDK1.8中还能看到 Segment 的数据结构,但是已经简化了属性,只是为了兼容旧版本;
② Hashtable(同一把锁) :使用 synchronized 来保证线程安全,效率非常低下。当一个线程访问同步方法时,其他线程也访问同步方法,可能会进入阻塞或轮询状态,如使用 put 添加元素,另一个线程不能使用 put 添加元素,也不能使用 get,竞争会越来越激烈效率越低。