面试中--问HashTable, HashMap, ConcurrentHashMap 之间的区别?

前言

学过数据结构的大家, 想必对 HashMap 都不陌生, HashMap 确实很简单嘛, 数据加链表嘛, 然后继续问你 HashTableHashMap 的区别是什么, 我们会说一个线程安全, 然后一个线程不安全啊, HashTable 是不允许存在 NULL 值的,  但是 HashMap 中的 key 和 value 都允许为 null , 并且 key 为 null 的键值对永远放在以 table[0] 为头节点的链表中. 我们知道 HashMap 是线程不安全的,只是用于单线程下, 而在多线程环境下就是采用 concurrent 并发包下的 ConcurrentHashMap .

为啥 Hashtable 是不允许 KEY 和 VALUE 为 null, 而 HashMap 则可以呢?

因为 Hashtable 在我们 put 空值的时候会直接抛空指针异常, 但是 HashMap 却做了特殊处理.

static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

而为啥 Hashtable 是不允许键或值为 null 的,但是 HashMap 的键值又都可以为 null 呢?

这是因为 Hashtable 使用的是 安全失败机制(fail-safe). 这种机制会使你此次读到的数据不一定是最新的数据. 如果你使用null值, 就会使得其无法判断对应的 key 是不存在还是为空, 因为你无法再调用一次 contain(key) 来对 key 是否存在进行判断, ConcurrentHashMap 同理。

HashMap

HashMap 是在 JDK1.2 引入Map的实现类.

HashMap是基于哈希表实现的, 每一个元素是一个 key-value 键值对, 其内部通过单链表解决冲突问题, 容量不足 (超过了阀值) 时, 同样会自动增长。

开始在JDK1.7的时候 底层数据结构是 数组+链表

在 JDK1.8 的时候, 底层数据结构是 数组+链表+红黑树 (看过源码的同学应该知道JDK1.8中即使用了单向链表, 也使用了双向链表, 双向链表主要是为了链表操作方便, 应该在插入, 扩容, 链表转红黑树, 红黑树转链表的过程中都要操作链表).

其次, HashMap 是非线程安全的, 只是用于单线程环境下, 多线程环境下可以采用 concurrent 并发包下的 concurrentHashMap.

HashMap 中 key 和 value 都允许为null . key 为 null 的键值对永远都放在以 table[0] 为头结点的链表中.

HashTable

Hashtable同样也是基于哈希表实现的,同样每个元素是一个 key-value 键值对,其内部也是通过单链表解决冲突问题,容量不足 (超过了阀值) 时,同样会自动增长.

Hashtable也是JDK1.0引入的类,是线程安全的,能用于多线程环境中.

Hashtable同样实现了Serializable接口,它支持序列化,实现了Cloneable接口,能被克隆.

也就是说,这两个东西大部分时相同的.

Hashtable 与 HashMap的不同
首先,从上面可以得出,线程安全是不同的.

HashTable只是在关键方法上加了synchronized,相当于针对HashTable本身加锁.

Hashtable 适合在多线程的情况下使用, 但是效率不高.

原因是: Hashtable 在对数据操作的时候都会上锁, 所以效率比较低

下面继续来说

HashMap 和 Hashtable 的区别:

1. 实现方式不同

Hashtable 继承了 Dictionary类,而 HashMap 继承的是 AbstractMap 类.

2. 初始化容量不同

HashMap 的初始容量是 16, Hashtable 初始容量是 11,  两者的负载因子默认都是 0.75.

3. 扩容机制不同

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

4. 迭代器不同

HashMap 中的 Iterator 迭代器是 fail-fast 的,而 Hashtable 的 Enumerator 不是 fail-fast 的.

快速失败(fail—fast)是java集合中的一种机制, 在用迭代器遍历一个集合对象时,如果遍历过程中对集合对象的内容进行了修改(增加、删除、修改),则会抛出Concurrent Modification Exception.

ConcurrentHashMap

分段锁技术

针对HashTable会锁整个hash表的问题,ConcurrentHashMap提出了分段锁的解决方案

分段锁的思想就是:锁的时候不锁整个hash表,而是只锁一部分

如何实现呢?这就用到了ConcurrentHashMap中最关键的Segment

ConcurrentHashMap中维护着一个Segment数组,每个Segment可以看做是一个HashMap而Segment本身继承了ReentrantLock,它本身就是一个锁. 在Segment中通过HashEntry数组来维护其内部的hash表. 每个 HashEntry 就代表了 map 中的一个 K-V,用 HashEntry 可以组成一个链表结构,通过next字段引用到其下一个元素.

看看源码:

public class ConcurrentHashMap<K, V> extends AbstractMap<K, V>
        implements ConcurrentMap<K, V>, Serializable {

    // ... 省略 ...
    /**
     * The segments, each of which is a specialized hash table.
     */
    final Segment<K,V>[] segments;

    // ... 省略 ...

    /**
     * Segment是ConcurrentHashMap的静态内部类
     * 
     * Segments are specialized versions of hash tables.  This
     * subclasses from ReentrantLock opportunistically, just to
     * simplify some locking and avoid separate construction.
     */
    static final class Segment<K,V> extends ReentrantLock implements Serializable {
    	// ... 省略 ...
    	/**
         * The per-segment table. Elements are accessed via
         * entryAt/setEntryAt providing volatile semantics.
         */
        transient volatile HashEntry<K,V>[] table;
        // ... 省略 ...
    }
    // ... 省略 ...

    /**
     * ConcurrentHashMap list entry. Note that this is never exported
     * out as a user-visible Map.Entry.
     */
    static final class HashEntry<K,V> {
        final int hash;
        final K key;
        volatile V value;
        volatile HashEntry<K,V> next;
        // ... 省略 ...
    }
}

因此, 只要我们的hash值足够分散,那么每次put的时候就会put到不同的segment中去。 而segment 自己本身就是一个锁,put的时候,当前segment会将自己锁住,此时其他线程无法操作这个segment, 但不会影响到其他segment的操作. 这个就是锁分段带来的好处.

线程安全的 put

ConcurrentHashMap 的 put() 源码如下:

public V put(K key, V value) {
    Segment<K,V> s;
    if (value == null)
        throw new NullPointerException();
    int hash = hash(key);
    int j = (hash >>> segmentShift) & segmentMask;

    // 根据key的hash定位出一个segment,如果指定index的segment还没初始化,则调用ensureSegment方法初始化
    if ((s = (Segment<K,V>)UNSAFE.getObject          // nonvolatile; recheck
         (segments, (j << SSHIFT) + SBASE)) == null) //  in ensureSegment
        s = ensureSegment(j);
    // 调用segment的put方法
    return s.put(key, hash, value, false);
}

最终会调用segment的put方法,将元素put到HashEntry数组中.

线程安全的扩容 (Rehash)

HashMap 的线程安全问题大部分出在扩容 (rehash) 的过程中.

ConcurrentHashMap 的扩容只针对每个 segment 中的 HashEntry 数组进行扩容.

由上述 put 的源码可知,ConcurrentHashMap 在 rehash 的时候是有锁的,所以在rehash的过程中,其他线程无法对 segment 的 hash 表做操作,这就保证了线程安全.

评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

即将秃头的菜鸟

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值