HashTable
- 底层数组+链表实现,key和value都不能为null。是线程安全的。线程安全实现方式是在修改数据的时候通过synchronized关键字锁住整个HashTable。效率比较低。ConcurrentHashMap做了相关的优化。
- 初始size为11,每次扩容都是当前的2倍+1:newSize=oldSize*2+1;
- 计算index方法:index=(hashcode&0x7FFFFFFF)% tab.length。(0x7FFFFFFF 32位int类型最大值)
HashMap:
- 底层也是数组+链表实现,key和value可以为null,非线程安全。
- 初始size为16,扩容是当前容量的2倍。size一定是2的n次幂。因为HashMap在计算元素位置的时候使用的是非常高效的位运算,按位与(tab.length-1 & hashCode),可以把元素均匀的分布在HasnMap中的数组上,减少hash碰撞,避免形成链表结构导致的查询效率降低。
- 扩容针对整个Map,每次扩容的时候,都重新计算原来数组中元素的位置并重新插入。
- 插入元素后才判断是否扩容,有可能产生无效扩容。即:扩容后没有再插入数据。 扩容因子:0.75,即当map元素中的总数超过Entry数组的75%时候,触发扩容操作,减少链表长度,使元素分配更均匀。
- 计算index方法:index=hash & (tab.length-1);
HashMap初始值还需要考虑加载因子。
- 哈希冲突:若干key的hash值按数组大小取模后,如果落在同一个数组下标上,将组成一条Entry链,对key的查找需要遍历Entry链上的每个元素,执行equals方法进行比较。
- 加载因子(扩容因子):为了降低HashCode碰撞概率,默认Map中键值对达到数组大小的75%时触发扩容。设置容量初始大小= 预估容量 / 0.75.
- 空间换时间:如果希望加快key查找时间,可以进一步降低加载因子,加大初始大小,降低hash碰撞概率。
HashMap和HashTable都是通过Hash算法来决定元素的存储位置,所以HashMap和HashTable的hash表包含以下属性:
- 容量(capacity):hash表中桶的数量(数组长度)
- 初始化容量(initial capacity):创建hash表时默认的桶数量(数组长度)。HashMap和HashTable可以再构造器中指定初始化容量。
- 尺寸(size):表示hash表中元素的数量。
- 负载因子:为size/capacity,负载因子为0,表示为空的hash表。0.5表示半满散列表。
除此之外,Hash表中还有个负载极限(扩容因子),决定了Hash表中最大填满度。当Hash表中的负载隐私达到指定的负载极限时,hash表将会自动的成倍增加容量。并将原有对象重新分配,放到新的桶中。
HashMap和HashTable的构造器允许指定一个扩容因子。默认为0.75.
扩容因子是时间和空间成本上的一种折中:
- 较高的负载因子可以降低Hash表所占用的存储空间。但会增加查询数据的事件开销。而get和put都要用到查询。
- 较小的负载因子可以提高查询性能,但是会增加Hash表锁占用的内存开销。
ConcurrentHashMap
- 底层采用分段数组+链表实现,线程安全。
- 通过把整个Map分成多个Segment,提供相同的线程安全。但是效率提成N倍。默认提升16倍。(读操作不加锁,由于HashEntry的Value变量是volatile的,能保证读取到最新的值)
- Hashtable是synchronized锁定整张Hash表,每次都锁定整张表让线程独占。ConcurrentHashMap允许多个修改操作并发执行,主要是用了锁分离技术。
- 有些方法需要跨段,比如size(),containsValue(),他们要锁定整个表,这需要按顺序锁定所有段,操作完毕后再按顺序释放所有段的锁。
- 扩容:段内扩容,段内元素超过该段对应Entry数组长度的75%触发扩容,不会对整个Map进行扩容。插入前检测是否需要扩容,避免无效扩容。
- 锁分段技术:将数据分成一段一段的存储,然后给每段数据配一把锁,当多个线程访问其中一段数据时,其他段的数据也能被其他线程访问。ConcurrentHashMap默认将Hash表分成16个段,get、put、remove等常用操作只需锁定当前所需要的桶,原来只有一个线程进入,现在可以有16个写线程执行。
总结
- Hashtable和hashMap都实现了Map接口,但是hashTable实现是基于Dictionary的ConcurrentHashMap是HashTable的替代。比HashTable的性能以及扩展性更好。
- HashMap基于Hash思想实现对数据的读写。当我们吧键值对通过put方法保存时,它调用key对象的hashCode方法计算hashcode。然后找到桶的位置来存储value对象。当获取对象时候,通过equals方法 找到正确的键值对,然后返回对象。两个key对象的hashcode相同时,他们会存储在同一个桶位置的链表中,通过键对象的equas方法来找键值对。如果链表长度超过阈值,会变为树形结构以提高查询效率。
- HashMap中null可以作为键,这样的键只有一个,但是可以有多个键对应的value为null,当get()方法返回null值,可以表示HashMap中没有改key,也可以表示key对应的value为null。所以在HashMap中,不能用get()方法判断HashMap是否存在某个个key。应该用containsKey方法判断。而在HashTable中不论是key还是value都不能为null。
- Hashtable与hashMap的另一个区别是迭代器,HashMap的迭代器是fail-fast迭代器,而HashTable的迭代器不是fail-fast,所以当有其他线程改变了HashMap结构,将会抛出异常。迭代器本身的remove方法移除元素不会排除异常。