HashMap,HashTable,ConcurrentHashMap的区别

HashMap:
实现:底层数组+链表。
初始大小16,扩容为oldsize*2。
key和value都可以为null,但只允许一个key为value
扩容针对整个Map,每次扩容时,原来数组中的元素依次重新计算存放位置,并重新插入
插入元素后才判断该不该扩容,有可能无效扩容(插入后如果扩容,如果没有再次插入,就会产生无效扩容)
当Map中元素总数超过Entry数组的75%,触发扩容操作,为了减少链表长度,元素分配更均匀
是线程不安全的,在多线程情况下put时可能导致元素丢失
原因:当多个线程同时执行addEntry(hash,key ,value,i)时,如果产生哈希碰撞,导致两个线程得到同样的bucketIndex去存储,就可能会发生元素覆盖丢失的情况。
多线程put时可能会导致get无限循环,具体表现为CPU使用率100%;
原因:在向HashMap put元素时,会检查HashMap的容量是否足够,如果不足,则会新建一个比原来容量大两倍的Hash表,然后把数组从老的Hash表中迁移到新的Hash表中,迁移的过程就是一个rehash()的过程,多个线程同时操作就有可能会形成循环链表,所以在使用get()时,就会出现Infinite Loop的情况

HashTable:
实现:底层数组+链表
初始大小11,当负载因子(size/capacity )达到0.75时便开始扩容,扩容为oldsize*2+1。
key和value都不可以为null
是线程安全的,所有方法被synchronized锁上,但是当有一个线程访问HashTable时,会将整个HashTable锁住,而其他线程只能在锁被释放后去访问,所以效率比较低。

ConcurrentHashMap:
底层采用分段的数组+链表实现,线程安全
通过把整个Map分为N个Segment,(默认为16个)可以提供相同的线程安全,但是效率提升N倍,默认提升16倍。(读操作不加锁,由于HashEntry的value变量是 volatile的,也能保证读取到最新的值。)
Hashtable的synchronized是针对整张Hash表的,即每次锁住整张表让线程独占,ConcurrentHashMap允许多个修改操作并发进行,其关键在于使用了锁分离技术。
有些方法需要跨段,比如size()和containsValue(),它们可能需要锁定整个表而而不仅仅是某个段,这需要按顺序锁定所有段,操作完毕后,又按顺序释放所有段的锁
扩容:段内扩容(段内元素超过该段对应Entry数组长度的75%触发扩容,不会对整个Map进行扩容),插入前检测需不需要扩容,有效避免无效扩容

锁分段技术:
首先将数据分成一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据的时候,其他段的数据也能被其他线程访问。

ConcurrentHashMap提供了与Hashtable和SynchronizedMap不同的锁机制。Hashtable中采用的锁机制是一次锁住整个hash表,从而在同一时刻只能由一个线程对其进行操作;而ConcurrentHashMap中则是一次锁住一个桶。

ConcurrentHashMap默认将hash表分为16个桶,诸如get、put、remove等常用操作只锁住当前需要用到的桶。这样,原来只能一个线程进入,现在却能同时有16个写线程执行,并发性能的提升是显而易见的。

在 ConcurrentHashMap 中,线程对映射表做读操作时,一般情况下不需要加锁就可以完成,对容器做结构性修改的操作才需要加锁。下面以 put 操作为例说明对 ConcurrentHashMap 做结构性修改的过程。
首先,根据 key 计算出对应的 hash 值:

Put 方法的实现

public V put(K key, V value) { 
        if (value == null)          //ConcurrentHashMap 中不允许用 null 作为映射值
            throw new NullPointerException(); 
        int hash = hash(key.hashCode());        // 计算键对应的散列码
        // 根据散列码找到对应的 Segment 
        return segmentFor(hash).put(key, hash, value, false); 
 }

根据 hash 值找到对应的 Segment

/** 
     * 使用 key 的散列码来得到 segments 数组中对应的 Segment 
     */
 final Segment<K,V> segmentFor(int hash) { 
    // 将散列值右移 segmentShift 个位,并在高位填充 0 
    // 然后把得到的值与 segmentMask 相“与”
 // 从而得到 hash 值对应的 segments 数组的下标值
 // 最后根据下标值返回散列码对应的 Segment 对象
        return segments[(hash >>> segmentShift) & segmentMask]; 
 }

最后,在这个 Segment 中执行具体的 put 操作:

.在 Segment 中执行具体的 put 操作

V put(K key, int hash, V value, boolean onlyIfAbsent) { 
            lock();  // 加锁,这里是锁定某个 Segment 对象而非整个 ConcurrentHashMap 
            try { 
                int c = count; 
 
                if (c++ > threshold)     // 如果超过再散列的阈值
                    rehash();              // 执行再散列,table 数组的长度将扩充一倍
 
                HashEntry<K,V>[] tab = table; 
                // 把散列码值与 table 数组的长度减 1 的值相“与”
                // 得到该散列码对应的 table 数组的下标值
                int index = hash & (tab.length - 1); 
                // 找到散列码对应的具体的那个桶
                HashEntry<K,V> first = tab[index]; 
 
                HashEntry<K,V> e = first; 
                while (e != null && (e.hash != hash || !key.equals(e.key))) 
                    e = e.next; 
 
                V oldValue; 
                if (e != null) {            // 如果键 / 值对以经存在
                    oldValue = e.value; 
                    if (!onlyIfAbsent) 
                        e.value = value;    // 设置 value 值
                } 
                else {                        // 键 / 值对不存在 
                    oldValue = null; 
                    ++modCount;         // 要添加新节点到链表中,所以 modCont 要加 1  
                    // 创建新节点,并添加到链表的头部 
                    tab[index] = new HashEntry<K,V>(key, hash, first, value); 
                    count = c;               // 写 count 变量
                } 
                return oldValue; 
            } finally { 
                unlock();                     // 解锁
            } 
        }

注意:这里的加锁操作是针对(键的 hash 值对应的)某个具体的 Segment,锁定的是该 Segment 而不是整个 ConcurrentHashMap。因为插入键 / 值对操作只是在这个 Segment 包含的某个桶中完成,不需要锁定整个ConcurrentHashMap。此时,其他写线程对另外 15 个Segment 的加锁并不会因为当前线程对这个 Segment 的加锁而阻塞。同时,所有读线程几乎不会因本线程的加锁而阻塞(除非读线程刚好读到这个 Segment 中某个 HashEntry 的 value 域的值为 null,此时需要加锁后重新读取该值)。

相比较于 HashTable 和由同步包装器包装的 HashMap每次只能有一个线程执行读或写操作,ConcurrentHashMap 在并发访问性能上有了质的提高。在理想状态下,ConcurrentHashMap 可以支持 16 个线程执行并发写操作(如果并发级别设置为 16),及任意数量线程的读操作。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值