Java jdk1.7、1.8 HashMap、HashTable、ConcorrectHashMap区别

336 阅读5分钟

1. HashMap与HashTable

与HashTable相比,HashMap的主要区别是: 1)HashMap是非同步的,即非线程安全的; 2)HashMap允许空键与空值; 3)在单线程环境中,HashMap的速度相比HashTable来说要快很多; 4)两者的内部遍历方式也不同,HashMap使用了fast-fail迭代器,HashTable在jdk1.8版本以后才使用。

1.1 同步与线程安全

HashMap允许不同的线程在同一时间对其中的内容进行修改,所以HashMap是非同步、非线性安全的。

HashTable通过Synchronized关键字来实现同步。 假如一个线程在对HashTable中的内容进行操作,HashTable将被锁住,别的线程再去调用HashTable将会报错。

同步可以保证线程安全,但也导致了效率下降。因此,JAVA 5中推出了HashTable的替代ConcurrentHashMap。

2. HashMap与ConcurrentHashMap

2.1 jdk1.7版本

2.1.1 HashMap

在jdk1.7版本及之前的版本当中,HashMap通过一个Entry数组来存储数据,通过key的hashcode取模来决定value的位置,不同的hashcode代表不同的存储位置,例如hashcode==i的key存储在Entry[i]。出现hash冲突,相同的hashcode会在这一位置形成一个链表,查找元素要先通过hashcode查找这一位置,复杂度O(1),再遍历链表确定key,复杂度O(n),所以链表越多,hashmap的查找效率越低,在1.8版本中过长的链表将加载为红黑树。

1.8版本以前HashMap扩容会将链表采用“头插法”的方法在新tab位置插入,这导致在并发状况下会发生死锁(在多线程同时resize时产生死锁,在get的时候导致不停循环,cpu占用率不断上升)。在1.7版本,头插法需要遍历链表,复杂度为O(n),在1.8版本改为尾插法,因为在链表转为红黑树后,遍历链表只需要O(logn)的复杂度。

死锁发生原理见:blog.csdn.net/qq_22158743…

2.1.2 ConcurrentHashMap

使用segment + hashentry来实现。segment在实现上继承了ReetrantLock,即实现了锁的功能。

ConcurrentHashMap可以理解为实现了分段锁功能,总共有16个segment。某一线程对其中一个segment进行操作不会影响其他线程对别的segment进行操作。

put实现

当执行put方法插入数据时,根据key的hash值,在Segment数组中找到相应的位置,如果相应位置的Segment还未初始化,则通过CAS进行赋值,接着执行Segment对象的put方法通过加锁机制插入数据。

CAS 乐观锁

Compare And Swap(CAS) 即比较与替换 CAS涉及到了三个基本操作数:内存地址V,旧的预期值A,要修改的新值B。

更新一个变量的时候,只有当变量的预期值A和内存地址V当中的实际值相同时,才会将内存地址V对应的值修改为B。

如果A与内存地址V中的实际值不相符(例如被其他线程操作而改变),程序会重新获取内存地址V的当前值,并重新计算想要修改的新值。该过程称为自旋。

2.2 jdk1.8版本

2.2.1 HashMap

通过链表形式来存储相同hashcode下的key,进行遍历查找需要O(n)的开销。 在jdk1.8版本后,HashMap弃用了原先的数据结构,采用Node数组来存储key和value。 如果key小于8个,Node采用链表进行存储,如果大于等于8个(>=8),会调用treeifyBin()函数,将链表转换为红黑树(注:如果tab[].length<64,仅进行扩容hashmap操作,不将链表转变为红黑树,具体见treeifyBin()函数)。

那么即使所有key的hashcode完全相同,由于红黑树的特点,查找某个特定元素,也只需要O(logn)的开销。

链表结点小于等于6个(<=6),红黑树将退化为链表。退化情况:

  1. removeTreeNode(),移除红黑树结点,若总结点小于等于6个则退化成链表,调用untreeify()函数。一下为removeTreeNode中判断源码:
    if (root == null
        || (movable
            && (root.right == null
                || (rl = root.left) == null
                || rl.left == null))) {
        tab[index] = first.untreeify(map);  // too small
        return;
  1. resize()过程后hashcode()发生变化,更加散列,调用split()函数对TreeNode进行分割,分割后小于等于6个则退化。以下为split()中其中一处退化(分割为两处):
if (lc <= UNTREEIFY_THRESHOLD)//<=6
    tab[index] = loHead.untreeify(map);

红黑树

红黑树是一种含有红黑结点并能自平衡的二叉查找树。它必须满足下面性质:

性质1:每个节点要么是黑色,要么是红色。
性质2:根节点是黑色。
性质3:每个叶子节点(NIL)是黑色。
性质4:每个红色结点的两个子结点一定都是黑色。
性质5:任意一结点到每个叶子结点的路径都包含数量相同的黑结点。

关于红黑树在插入和删除后的自平衡方法,可以参见其余博主。 关于HashMap其他内容,插入(putval)及resize(),可见: blog.csdn.net/login_sonat…

2.2.2 ConcurrentHashMap

在jdk1.7中的ConcurrentHashMap效率仍存在很大的问题,即遍历链表查询效率过低,在jdk1.8中进行了优化。

放弃了segment的设计,取而代之的是Node+CAS+Synchronized来保证并发安全。 与HashMap类似,Node在key超过一定数量时转变为红黑树来提高查询效率。 通过CAS和Synchronized来保证并发的安全性。

ConcurrentHashMap源码分析可见: blog.csdn.net/hezuo1181/a…

ConcurrentHashMap transfer()源码分析可见: www.jianshu.com/p/77fda250b…