上个关于Map集合的博客,介绍了Map集合的框架和HashMap的一些知识,比较了一下jdk1.7与jdk1.8中HashMap的不同,这篇博客,我们来比较一下HashMap和Hashtable还有ConcurrentHashMap。
·(一)首先说一下HashMap和Hashtable的区别
1.HashMap是Hashtable的非线程安全的实现,所以Hashtable的方法是线程安全的,所以就效率而言,HashMap的效率比Hashtable的效率要高一些
2.HashMap支持键值为null(最多只允许一条记录的值为null,不允许多条记录的值为null),Hashtable不支持
3.在类的继承实现关系上,HashMap继承了AbstractMap类,而Hashtable则继承了Dictionary类
4.在使用上,两个的迭代器不同,HashMap是Iterator,而Hashtable则是Enumeration
5.在存储上,扩容机制不同,上个博客我们讲了HashMap的扩容机制是*2,而Hashtable则是*2+1,其中HashMap的默认大小是16,而且并且一定是2的指数
对于第一点来说,Hashtable虽然是线程安全的,但是其实现的策略实现代价太大了,就是给put和get相关操作加了一个synchronized的关键字,就相当于给哈希表加了一把大锁。其实就和我之前在单例模式中,有一个懒汉的实现,那个是线程不安全的,所以我下面给出了一个只加了一个同步关键字的实现是一样的,效率实在是太低了,当竞争激烈的并发场景中应用性就太差了
所以引出了下面的ConcurrentHashMap
·(二)在并发环境下产生的ConcurrentHashMap
为了应对HashMap在并发环境下不安全的情况,于是ConcurrentHashMap产生了,其实现大量用了volatile,final,CAS等lock-free技术来减少锁竞争对性能的影响
与HashMap一样,ConcurrentHashMap在jdk1.7和jdk1.8中也是不一样的,下面谈一下它们的区别
·jdk1.7中的ConcurrentHashMap
在jdk1.7中,ConcurrentHashMap采用了数组+Segment+分段锁的方式实现
首先我们来解释一下什么是Segment
ConcurrentHashMap中的分段锁称Segment,类似于HashMap的结构,内部拥有一个Entry数组,数组中每个元素又是一个链表,同时又是一个ReetrantLock
然后我们来看一下ConcurrentHashMap的内部结构
这样查询一个元素,首先是定位到Segment然后Hash定位到元素所在的链表的头部。ConcurrentHashMap每一个Segment都有一个锁,所以这可以达到并发得访问各个Segment的Entry。
这样来看,写操作的时候,只对元素所在的Segment进行加锁就可以,不会影响到其他Segment,所以并发性可以得到很好的提高,这里我们用put方法的源码来解释一下
public V put(K key, V value) {
Segment<K,V> s;
if (value == null)
throw new NullPointerException();//第一次求hash值,确定segment数组的索引位置
int hash = hash(key);
int j = (hash >>> segmentShift) & segmentMask;
if ((s = (Segment<K,V>)UNSAFE.getObject // nonvolatile; recheck
(segments, (j << SSHIFT) + SBASE)) == null) // in ensureSegment
s = ensureSegment(j);
return s.put(key, hash, value, false);
}
final V put(K key, int hash, V value, boolean onlyIfAbsent) {
HashEntry<K,V> node = tryLock() ? null :
scanAndLockForPut(key, hash, value);
V oldValue;
try {
HashEntry<K,V>[] tab = table;//确定了key的hash值所在HashEntry中索引位置,同HashMap,我在上一篇中分析过这里
int index = (tab.length - 1) & hash;//获得了链表的头部结点
HashEntry<K,V> first = entryAt(tab, index);
for (HashEntry<K,V> e = first;;) {//下面分类讨论了,存的情况,如果头部为null或者不为null的情况//就是为空的话就放进去;不为空但是找到了相同的key就替换,没找到,就放到链表头部
if (e != null) {
K k;
if ((k = e.key) == key ||
(e.hash == hash && key.equals(k))) {
oldValue = e.value;
if (!onlyIfAbsent) {
e.value = value;
++modCount;
}
break;
}
e = e.next;
}
else {if (node != null)
node.setNext(first);
else
node = new HashEntry<K,V>(hash, key, value, first);
int c = count + 1;
if (c > threshold && tab.length < MAXIMUM_CAPACITY)//这里进行了扩容
rehash(node);
else
setEntryAt(tab, index, node);//修改的次数+1
++modCount;
count = c;
oldValue = null;
break;
}
}
} finally {
unlock();
}
return oldValue;
}
下面的代码很关键,就是先获得Segment的位置;然后获取锁,获得当前Segment的HashEntry数组,然后对key进行二次hash,确定在数组中的位置 (tab.length - 1) & hash;然后遍历进行替换等操作;最后关闭了锁。
·jdk1.8中的ConcurrentHashMap
jdk1.8中的ConcurrentHashMap参考了jak1.8的HashMap中的实现,采用了数组+链表+红黑树的实现方式设计,其中大量采用了CAS操作,先简单介绍下CAS吧
CAS:基于锁的操作,是一个乐观锁,就是采用了一种宽泛的态度,通过某种方式不加锁来处理资源。
jdk1.8中采用的是Node,其思想也不是1.7中的分段锁概念,
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
volatile V val;
volatile Node<K,V> next;......省略
}
其中val和next用volatile修饰,保证了并发的可见性
接下来再来看一下ConcurrentHashMap的结构
和之前博客的介绍一样,红黑树是一种性能很好的二叉查找树,查找性能是O(logn);和HashMap一样,当链表结点数量大于8的时候,会将链表转化为红黑树进行存储
总的来说jdk1.8中的ConcurrentHashMap采用了synchronezed+CAS+HashEntry+红黑树,取消了分段锁的概念,引入了数组+链表+红黑树的结构;其实通过CAS+synchronized保证线程的安全。