ConcurrentHashMap & HashTable

ConcurrentHashMap & HashTable

在HashMap中存在线程安全问题,并发的多线程使用场景中使用HashMap可能造成死循环,针对这个问题有三个解决方案

  • 使用Collections.synchronizedMap(Map)创建线程安全的map集合
  • Hashtable
  • ConcurrentHashMap

不过出于线程并发度的原因,通常使用最后的ConcurrentHashMap,他的性能和效率明显高于前两者

HashTable

HashTable低层和HashMap基本一样,只不过为了保证线程安全,在对数据操作的每个方法上都加了锁
在这里插入图片描述
除此之外还有些不同

① 实现方式不同
Hashtable 继承了 Dictionary类,而 HashMap 继承的是 AbstractMap 类

② 初始化容量不同
HashMap 的初始容量为:16,Hashtable 初始容量为:11,两者的负载因子默认都是:0.75

之所以会有这样的不同,是因为Hashtable和HashMap设计时的侧重点不同。Hashtable的侧重点是哈希的结果更加均匀,使得哈希冲突减少。当哈希表的大小为素数时,简单的取模哈希的结果会更加均匀。而HashMap则更加关注hash的计算效率问题。在取模计算时,如果模数是2的幂,那么我们可以直接使用位运算来得到结果,效率要大大高于做除法。HashMap为了加快hash的速度,将哈希表的大小固定为了2的幂。当然这引入了哈希分布不均匀的问题,所以HashMap为解决这问题,又对hash算法做了一些改动。这从而导致了Hashtable和HashMap的计算hash值的方法不同

③ 扩容机制不同
当现有容量大于总容量 * 负载因子时,HashMap 扩容规则为当前容量翻倍,Hashtable 扩容规则为当前容量翻倍 + 1

④ 迭代器不同
HashMap 中的 Iterator 迭代器是 fail-fast 的,而 Hashtable 的 Enumerator 不是 fail-fast 的,所以,当其他线程改变了HashMap 的结构,如:增加、删除元素,将会抛出ConcurrentModificationException 异常,在JDK8之前的版本中,Hashtable是没有fast-fail机制的。在JDK8及以后的版本中 ,HashTable也是使用fast-fail的

⑤ Hashtable既不支持Null key也不支持Null value
可以看到当value为空的时候会报空指针异常
在这里插入图片描述
hash值是通过key.hashCode()获得的,所以key也不能为null

在这里插入图片描述
但是对于hashMap中对null值有特殊的处理,null可以作为键,这样的键只有一个,放在第一位
在这里插入图片描述
可以有一个或多个键所对应的值为null。当get()方法返回null值时,可能是 HashMap中没有该键,也可能使该键所对应的值为null。因此,在HashMap中不能由get()方法来判断HashMap中是否存在某个键, 而应该用containsKey()方法来判断

由于无论读写他都是给整个集合假设,所以在同一时间其他的线程会为之阻塞,效率低下

ConcurrentHashMap
jdk1.7

同HashMap一样,Segment包含一个HashEntry数组,数组中的每一个HashEntry既是一个键值对,也是一个链表的头节点
在这里插入图片描述
Segment 是 ConcurrentHashMap 的一个内部类,主要的组成如下

static final class Segment<K,V> extends ReentrantLock implements Serializable {

    private static final long serialVersionUID = 2249069246763182397L;

    // 和 HashMap 中的 HashEntry 作用一样,真正存放数据的桶
    transient volatile HashEntry<K,V>[] table;

    transient int count;
        // 记得快速失败(fail—fast)么?
    transient int modCount;
        // 大小
    transient int threshold;
        // 负载因子
    final float loadFactor;

}

HashEntry跟HashMap差不多的,但是不同点是,他使用volatile去修饰了他的数据Value还有下一个节点next,这样保证了多线程下的可见性,有序性和原子性

ConcurrentHashMap 采用了分段锁技术,其中 Segment 继承于 ReentrantLock,不会像 HashTable 那样不管是 put 还是 get 操作都需要做同步处理,理论上 ConcurrentHashMap 支持 CurrencyLevel (Segment 数组数量)的线程并发,每当一个线程占用锁访问一个 Segment 时,不会影响到其他的 Segment

这样操作的时候会先定位到Segment,然后再进行操作
对于put
① 为输入的Key做Hash运算,得到hash值。
② 通过hash值,定位到对应的Segment对象
③ 获取可重入锁
④ 再次通过hash值,定位到Segment当中数组的具体位置。
⑤ 插入或覆盖HashEntry对象。
⑥ 释放锁

final V put(K key, int hash, V value, boolean onlyIfAbsent) {
          // 将当前 Segment 中的 table 通过 key 的 hashcode 定位到 HashEntry
            HashEntry<K,V> node = tryLock() ? null :
                scanAndLockForPut(key, hash, value);
            V oldValue;
            try {
                HashEntry<K,V>[] tab = table;
                int index = (tab.length - 1) & hash;
                HashEntry<K,V> first = entryAt(tab, index);
                for (HashEntry<K,V> e = first;;) {
                    if (e != null) {
                        K k;
 // 遍历该 HashEntry,如果不为空则判断传入的 key 和当前遍历的 key 是否相等,相等则覆盖旧的 value。
                        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 {
                 // 不为空则需要新建一个 HashEntry 并加入到 Segment 中,同时会先判断是否需要扩容。
                        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);
                        ++modCount;
                        count = c;
                        oldValue = null;
                        break;
                    }
                }
            } finally {
               //释放锁
                unlock();
            }
            return oldValue;
        }

而对于get方法来说

由于 HashEntry 中的 value 属性是用 volatile 关键词修饰的,保证了内存可见性,所以每次获取时都是最新值,所以整个过程都不需要加锁
① 为输入的Key做Hash运算,得到hash值。
② 通过hash值,定位到对应的Segment对象
③ 再次通过hash值,定位到Segment当中数组的具体位置

对于size方法

他的设计思想实际上和乐观锁的设计思想如出一辙,为了尽量不锁住所有Segment,首先乐观地假设Size过程中不会有修改。当尝试一定次数,才无奈转为悲观锁,锁住所有Segment保证强一致性
① 遍历所有的Segment。

② 把Segment的元素数量累加起来。

③ 把Segment的修改次数累加起来。

④ 判断所有Segment的总修改次数是否大于上一次的总修改次数。如果大于,说明统计过程中有修改,重新统计,尝试次数+1;如果不是。说明没有修改,统计结束。

⑤ 如果尝试次数超过阈值,则对每一个Segment加锁,再重新统计。

⑥ 再次判断所有Segment的总修改次数是否大于上一次的总修改次数。由于已经加锁,次数一定和上次相等。

⑦ 释放锁,统计结束

jdk 1.8

在JDK1.8中,放弃了Segment臃肿的设计,取而代之的是采用Node + CAS + Synchronized来保证并发安全进行实现,synchronized只锁定当前链表或红黑二叉树的首节点,这样只要hash不冲突,就不会产生并发,效率又提升N倍
在这里插入图片描述

注意

ConcurrentHashMap只能保证单一操作的线程安全,如果是组合操作并不能保证线程安全,比如下面的一段代码并不是线程安全的

ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();
map.put("A",1);
int i = 2;
map.put("A",2);

对于上面的问题,concurrentHashMap给我们提供了一个方法解决,CAS的思想
在这里插入图片描述

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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值