本文主要讨论Map家族的区别和联系,个体详细内容单独介绍。
区别:
- 线程安全问题:HashTable(修改时锁住整个HashTable)、ConcurrentHashMap(CAS)是线程安全的(),HashMap是线程不安全的;
- 性能方面:HashMap > ConcurrentHashMap > HashTable;
- HashMap k, v 均支持null,HashTable与ConcurrentHashMap均不支持。
- 容量:
HashTable | HashMap | ConcurrentHashMap | |
---|---|---|---|
初始容量 | 11 | 16 | 16 |
扩容因子 | 0.75 | 0.75 | 0.75 |
扩容公式 | oldSize*2+1 | oldSize*2 | oldSize*2 |
补充:
一. HashMap线程不安全的表现:
- put()方法:两个线程put的key,HashCode发生碰撞。在获取了相同的数组下标或父节点,写入时后一个会将前一个的值覆盖,从而造成数据的丢失。
- put()时,若++size>threshold则会触发resize()方法进行扩容。resize()会重新排列Map内的entry,多线程的情况下会产生闭合链表(1.8之前采用头插法会导致此结果)导致get的时候出现死循环;
- remove()方法:线程一获取删除地址后挂起,线程二删除后,线程一按原地址删除可能会删除错误的目标。
二. (1.8之前)循环链表的产生:
Entry[] table是全局变量,多个线程操纵同一个实例table在多个线程是共享的。
for(Entry<k,v> e : table){
while(null!=e){
Entry<k,v> next = e.next;
if(rehash){
e.hash = null == e.key ? 0 : hash(e.key);
}
int i = indexFor(e.hash, newCapacity);
//头插法
e.next = newTable[i];
newTable[i] = e;
e = next;
}
}
假设有两个线程:线程一和线程二。当线程一执行到Entry<k, v> next = e.next;时资源耗尽,线程二切入并执行完毕。此时由于table为线程一与线程二共享,因此table值变为线程二扩容后的值。
此时e->key3,next->key7;
将key3插入线程一的newTable,key3.next->null,e->key7;
此时经过线程二key7.next->key3,所以next->key3,e.next = newTable[i]导致key7.next->key3;
继续执行e->key3,e.next=newTable[i]也就是key7,就形成了一个闭环。
会导致死循环使CPU资源耗尽。
三. 为何在多线程的情况下(HashTable、ConcurrentHashMap)不支持null?
当你通过get(k)获取对应的value时,如果获取到的是null时,你无法判断,它是put的时候value为null,还是这个key从来没有做过映射。而HashMap是非并发的,可以通过contains(key)来做这个判断。而支持并发的Map在调用m.contains(key)和m.get(key),m很可能已经被其他线程改变了。