谈一谈HashTable、HashMap以及ConcurrentHashMap

146 阅读5分钟

HashTable、HashMap、ConcurrentHashMap均为数据结构中“映射表”或者说“字典”的实现。

这种数据结构具备的能力是:给出一个键,它能在集合里快速找到指定的对象。

Hashtable

Hashtable是Java集合类库中对字典数据结构的早期实现,它是线程安全的,在HashTable里,众多的方法都用synchronized加锁,所以它的锁粒度非常大,并发能力并不好,而且在目前的注释里,Hashtable 是保留类不建议使用,推荐在单线程环境下使用 HashMap 替代,如果需要多线程使用则用ConcurrentHashMap 替代。

 * Java Collections Framework</a>.  Unlike the new collection
 * implementations, {@code Hashtable} is synchronized.  If a
 * thread-safe implementation is not needed, it is recommended to use
 * {@link HashMap} in place of {@code Hashtable}.  If a thread-safe
 * highly-concurrent implementation is desired, then it is recommended
 * to use {@link java.util.concurrent.ConcurrentHashMap} in place of
 * {@code Hashtable}.

HashMap

HashMap是最经典也最为常用的Java字典容器,它的底层中有一个数组结构,通过传入key的hashcode来决定元素的位置(数组下标)。当然,不一样的key有可能会计算出一样的hashcode,所以数组元素有可能会是一个链表结构,存放一个又一个hashcode一样,但是又不一样的对象(不一样体现在对象的equals方法的对比是不一样的)。

值得一提的是,HashMap类也经历了较大的改变,以上所谓基于数组和链表的实现是JDK1.7及以前的,而Java8开始,底层不仅使用了数组和链表,还使用了红黑树的数据结构。

HashMap有两种机制:

  1. 扩容机制,当运行时发现元素总个数达到容量的0.75(这是一个阈值,也可以自定义)时,会展开一次数组的扩容:会将数组扩大一遍,并且对所有的节点进行重新hash计算,并挨个摆放。
  2. (JDK1.8开始)树化机制,当运行时链表的总个数达到8(一个阈值)时,会立刻对这一链表进行树化,也就是说,这一条链表的元素类型将从普通的Node变成TreeNode,也就是红黑树的节点。所以说,底层的数组的类型是Node类,而TreeNode是Node类的子类。

为什么JDK8开始,HashMap要使用红黑树数据结构?

采用红黑树数据结构的目的,要从链表的劣势开始说起。

我们考虑一种特殊情况,hashcode分布不均匀的情况,这会导致链表变长,那么查找效率就会变低,因为链表的查找平均复杂度是O(n/2)。

而红黑树:

它是在二叉查找树的基础上给节点增加红黑颜色属性以及五条约束的性质。

二叉查找树是一种二叉树,它约束每一个节点的左子树值小于它本身,右子数的值大于它本身。这样利用二分的查找思想,提高了查找的效率,但是当二叉查找树不断偏向一边,那就会看起来像是瘸了。。。

红黑树解决了以上问题,它的五条约束:

  • 每个节点非红即黑
  • 根节点总是黑色的
  • 如果节点是红色的,则它的子节点必须是黑色的(反之不一定)
  • 每个叶子节点都是黑色的空节点(NIL节点)
  • 从根节点到叶节点或空子节点的每条路径,必须包含相同数目的黑色节点(即相同的黑色高度)

红黑树是自平衡的,当对红黑树进行操作破坏平衡时,会利用 变色、左旋、右旋 来达到平衡。

红黑树的查找时间复杂度是O(logn),这就是为什么HashMap引入红黑树的原因。

那么HashMap有什么方法来减少hashcode重复的问题呢?

hash(hash不等于hashcode,hash是HashMap中运算一个key的hash值,而运算中用到了key对象的hashcode方法)的计算上:

首先计算式是:

return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16)

上面的计算式里,key为空的话,hash就是0,所以HashMap里,键为空的话,也会指向一个对象的。

如果不为空,那么运算是:

key对象的hashcode()方法的值h 与 h右移16位的值 进行亦或。

这一运算直接导致:hashcode不止与低位有关,还与高位有关。

接下来看看数组大小:

capacity这个构造参数在HashMap用于表达数组大小。

capacity只能是2的幂次,如4 8 16 32等,而这也是有原因的。

当HashMap计算出了一个key的hash,如何定位它呢?

我们知道,一个长度为9的数组,要使一个int类型数字能定位到数组的位置,进行一个取余运算就可以了:

int position = hash % capacity;

但是HashMap并不是这样做的,这样的运算很麻烦。

HashMap利用了位运算的高效:

i = (n - 1) & hash]

n就是数组(桶)的大小,这个位与的好处就是很快就能计算出低位下的值,并作为位置

HashMap中put方法的过程?

调用哈希函数获取Key对应的hash值,再计算其数组下标;

如果没有出现哈希冲突,则直接放入数组;如果出现哈希冲突,则以链表的方式放在链表后面;

如果链表长度超过阀值( TREEIFY THRESHOLD==8),就把链表转成红黑树,链表长度低于6,就把红黑树转回链表;

如果结点的key已经存在,则替换其value即可;

如果集合中的键值对大于12,调用resize方法进行数组扩容。