HashMap、Hashtable和ConcurrentHashMap的对比
导语
之前学习了哈希表的扩容机制(以HashMap为例)和哈希冲突的解决办法,分别在:
哈希表的扩容实现机制 和解决哈希冲突的几种方法(距离推演)两篇博客中进行了总结记录。
而这篇主要是对比一下Java中三种哈希表的实现细节与应用场景。这也是面试中经常遇到的问题。
HashMap
HashMap的底层实现是链表+数组的形式,或者说是一个链表的数组(数组的元素为链表)。
实现细节
HashMap的初始容量为16,默认负载极限是0.75。
当负载因子(=元素个数/哈希表容量)达到负载极限时,触发HashMap的扩容。
HashMap的扩容方式是,newsize = oldsize * 2 ,所以HashMap的容量都是2的幂。扩容发生时,先将整个旧表中的元素转移到新表中,再插入新的元素。
put(K key,V value)
当调用put(K key, V value)时:
如果key不为null:
- 先调用key.hashCode(),获取key的哈希值,然后计算下标。
如果Entry数组下标为i的位置无元素,则新增元素;
如果Entry数组下标为i的位置有元素,则检查有没有key值相等的:
如果无,则新增元素在Entry的末尾,否则替换掉原先的value,并返回原value。
如果key为null:
- 先调用putForNullKey(V value)方法。
获取Entry的第一个元素table[0],并基于第一个元素的next属性开始遍历,直到找到key为null的Entry,将其value设置为新的value值。
如果没有找到key为null的元素,则调用addEntry(0, null, value, 0);增加一个新的entry。
get (K key)
当调用get(K key)时:
如果key不为null:
- 先调用key.hashCode(),获取key的哈希值,然后计算下标。
在Entry数组下标为i的位置遍历,有就返回value,无就返回null。
如果key为null:
- 先调用getForNullKey()方法。
获取Entry的第一个元素table[0],并基于第一个元素的next属性开始遍历,找到则返回value,找不到则返回null。
需要注意的是:
由于HashMap支持储存为null的value,所以不能用get(key)==null来判断是否存在某个值,而应该调用containsKey(key)方法。
Hashtable
Hashtable的底层实现也是链表+数组的形式。
实现细节
Hashtable的初始容量为11,默认负载极限是0.75。
当负载因子(=元素个数/哈希表容量)达到负载极限时,触发HashMap的扩容。
Hashtable的扩容方式是,newsize = oldsize * 2 + 1。
扩容发生时,先将整个旧表中的元素转移到新表中,再插入新的元素。
线程安全
Hashtable和HashMap的一大区别在于,HashMap是非线程安全的,而Hashtable是线程安全的。
HashMap不支持多线程,如果要解决线程同步问题,可以通过下面的语句进行同步:
Map m = Collections.synchronizeMap(hashMap);
返回的是一个同步的map对象,里面封装了hashMap对象所有的方法。
Hashtable是线程安全的,实现同步的方式是给整个表加上synchronized关键字,所以任意两个线程不能同时访问这张表。
虽然实现了线程同步,但是将整张表锁住的方式性能很低。
put(K key, V value)
当调用put(K key, V value)时:
如果key为null,就抛出空指针异常。
如果key不为null,获取key的哈希值,计算下标。
如果存在key值一致的键值对,替换掉原先的value,并返回原value值;否则,则插入元素在Entry末尾。
get (K key)
当调用get(K key)时:
先调用key.hashCode(),获取key的哈希值,然后计算下标。
在Entry数组下标为i的位置遍历,有就返回value,无就返回null。
ConcurrentHashMap
ConcurrentHashMap是基于Hashtable的改进,其底层实现也是链表+数组的形式,不过是分段式的。
实现细节
ConcurrentHashMap的初始容量为16个桶。
当某个桶的负载因子(=元素个数/哈希表容量)达到负载极限时,触发该桶(仅仅是当前段)的扩容,而不是整张表的扩容。
ConcurrentHashMap的扩容方式是段内扩容,每次扩容为原先的两倍,且可以直接判断哈希码对链表长度取余的结果是0还是1,决定将其放在低位或者高位。
线程安全
线程同步的实现是利用锁分离技术。不像Hashtable对整张表加锁,而是对当前段加锁。这支持多个线程并发进行读操作和写操作。并且效率远远高于Hashtable。
当某些操作需要对整张表加锁时,就依次对所有段加锁;操作完成后,再依次释放所有的锁。
对比总结
HashMap | Hashtable | ConcurrentHashMap | |
---|---|---|---|
实现方式 | 数组+链表 | 数组+链表 | 分段的数组+链表 |
是否线程安全 | × | √ | √ |
多线程如何实现 | - | 对整个表加锁 | 锁分离技术 |
扩容方式 | newsize = oldsize*2 | newsize = 2*oldsize + 1 | 段内扩容 |
是否允许key为null? | √,但最多只有一个key的值null | × | × |
是否允许value为null? | √ | × | × |
迭代器 | Iterator是fail-fast迭代器 | enumerator迭代器不是fail-fast的 | enumerator迭代器不是fail-fast的 |
结束语
本篇查阅了很多资料,主要介绍了关于HashMap、Hashtable和ConcurrentHashMap这三种常用哈希表的对比,主要是关于线程安全如何实现、如何进行扩容、如何查询和添加数据等等。
学习过程中,还了解到了HashSet,和HashMap类似。不同点在于HashMap中存放的是键值对,而HashSet只存放key值(底层实现是使用HashMap,value值全部相同)。
但是对于哈希表我还存在一些疑问悬而未决。比如哈希表中的删除问题,如何进行遍历,fail-fast迭代器的含义等等,需要后续学习中进一步深入了解。