【数据结构之哈希表(三)】 HashMap、Hashtable和ConcurrentHashMap的对比

Java哈希表对比
本文对比分析了Java中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。
当某些操作需要对整张表加锁时,就依次对所有段加锁;操作完成后,再依次释放所有的锁。

对比总结

HashMapHashtableConcurrentHashMap
实现方式数组+链表数组+链表分段的数组+链表
是否线程安全×
多线程如何实现-对整个表加锁锁分离技术
扩容方式newsize = oldsize*2newsize = 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迭代器的含义等等,需要后续学习中进一步深入了解。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值