Java里的Map集合(二)HashMap和Hashtable和ConcurrentHashMap的比较

上个关于Map集合的博客,介绍了Map集合的框架和HashMap的一些知识,比较了一下jdk1.7与jdk1.8中HashMap的不同,这篇博客,我们来比较一下HashMap和Hashtable还有ConcurrentHashMap。

 

·(一)首先说一下HashMap和Hashtable的区别

1.HashMap是Hashtable的非线程安全的实现,所以Hashtable的方法是线程安全的,所以就效率而言,HashMap的效率比Hashtable的效率要高一些

2.HashMap支持键值为null(最多只允许一条记录的值为null,不允许多条记录的值为null),Hashtable不支持

3.在类的继承实现关系上,HashMap继承了AbstractMap类,而Hashtable则继承了Dictionary类

4.在使用上,两个的迭代器不同,HashMap是Iterator,而Hashtable则是Enumeration

5.在存储上,扩容机制不同,上个博客我们讲了HashMap的扩容机制是*2,而Hashtable则是*2+1,其中HashMap的默认大小是16,而且并且一定是2的指数

对于第一点来说,Hashtable虽然是线程安全的,但是其实现的策略实现代价太大了,就是给put和get相关操作加了一个synchronized的关键字,就相当于给哈希表加了一把大锁。其实就和我之前在单例模式中,有一个懒汉的实现,那个是线程不安全的,所以我下面给出了一个只加了一个同步关键字的实现是一样的,效率实在是太低了,当竞争激烈的并发场景中应用性就太差了

所以引出了下面的ConcurrentHashMap

·(二)在并发环境下产生的ConcurrentHashMap

为了应对HashMap在并发环境下不安全的情况,于是ConcurrentHashMap产生了,其实现大量用了volatile,final,CAS等lock-free技术来减少锁竞争对性能的影响

与HashMap一样,ConcurrentHashMap在jdk1.7和jdk1.8中也是不一样的,下面谈一下它们的区别

·jdk1.7中的ConcurrentHashMap

在jdk1.7中,ConcurrentHashMap采用了数组+Segment+分段锁的方式实现

首先我们来解释一下什么是Segment

ConcurrentHashMap中的分段锁称Segment,类似于HashMap的结构,内部拥有一个Entry数组,数组中每个元素又是一个链表,同时又是一个ReetrantLock

然后我们来看一下ConcurrentHashMap的内部结构

 

这样查询一个元素,首先是定位到Segment然后Hash定位到元素所在的链表的头部。ConcurrentHashMap每一个Segment都有一个锁,所以这可以达到并发得访问各个Segment的Entry。 

这样来看,写操作的时候,只对元素所在的Segment进行加锁就可以,不会影响到其他Segment,所以并发性可以得到很好的提高,这里我们用put方法的源码来解释一下

public V put(K key, V value) {
        Segment<K,V> s;
        if (value == null)
            throw new NullPointerException();

        //第一次求hash值,确定segment数组的索引位置
        int hash = hash(key);
        int j = (hash >>> segmentShift) & segmentMask;
        if ((s = (Segment<K,V>)UNSAFE.getObject          // nonvolatile; recheck
             (segments, (j << SSHIFT) + SBASE)) == null) //  in ensureSegment
            s = ensureSegment(j);
        return s.put(key, hash, value, false);
    }

final V put(K key, int hash, V value, boolean onlyIfAbsent) {
            HashEntry<K,V> node = tryLock() ? null :
                scanAndLockForPut(key, hash, value);
            V oldValue;
            try {
                HashEntry<K,V>[] tab = table;

                //确定了key的hash值所在HashEntry中索引位置,同HashMap,我在上一篇中分析过这里
               int index = (tab.length - 1) & hash;

                //获得了链表的头部结点
                HashEntry<K,V> first = entryAt(tab, index);
                for (HashEntry<K,V> e = first;;) {//下面分类讨论了,存的情况,如果头部为null或者不为null的情况

                //就是为空的话就放进去;不为空但是找到了相同的key就替换,没找到,就放到链表头部
                    if (e != null) {
                        K k;
                        if ((k = e.key) == key ||
                            (e.hash == hash && key.equals(k))) {
                            oldValue = e.value;
                            if (!onlyIfAbsent) {
                                e.value = value;
                                ++modCount;
                            }
                            break;
                        }
                        e = e.next;
                    }
                    else {

                        if (node != null)
                            node.setNext(first);
                        else
                            node = new HashEntry<K,V>(hash, key, value, first);
                        int c = count + 1;
                        if (c > threshold && tab.length < MAXIMUM_CAPACITY)

                           //这里进行了扩容
                            rehash(node);
                        else
                            setEntryAt(tab, index, node);

                      //修改的次数+1
                        ++modCount;
                        count = c;
                        oldValue = null;
                        break;
                    }
                }
            } finally {
                unlock();
            }
            return oldValue;
        }

下面的代码很关键,就是先获得Segment的位置;然后获取锁,获得当前Segment的HashEntry数组,然后对key进行二次hash,确定在数组中的位置 (tab.length - 1) & hash;然后遍历进行替换等操作;最后关闭了锁。

·jdk1.8中的ConcurrentHashMap

jdk1.8中的ConcurrentHashMap参考了jak1.8的HashMap中的实现,采用了数组+链表+红黑树的实现方式设计,其中大量采用了CAS操作,先简单介绍下CAS吧

CAS:基于锁的操作,是一个乐观锁,就是采用了一种宽泛的态度,通过某种方式不加锁来处理资源。

jdk1.8中采用的是Node,其思想也不是1.7中的分段锁概念,

 static class Node<K,V> implements Map.Entry<K,V> {
        final int hash;
        final K key;
        volatile V val;
        volatile Node<K,V> next;

        ......省略

}

其中val和next用volatile修饰,保证了并发的可见性

接下来再来看一下ConcurrentHashMap的结构

 

和之前博客的介绍一样,红黑树是一种性能很好的二叉查找树,查找性能是O(logn);和HashMap一样,当链表结点数量大于8的时候,会将链表转化为红黑树进行存储

总的来说jdk1.8中的ConcurrentHashMap采用了synchronezed+CAS+HashEntry+红黑树,取消了分段锁的概念,引入了数组+链表+红黑树的结构;其实通过CAS+synchronized保证线程的安全。

 

 

 

 

 

 

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值