前言
学过数据结构的大家, 想必对 HashMap 都不陌生, HashMap 确实很简单嘛, 数据加链表嘛, 然后继续问你 HashTable 和 HashMap 的区别是什么, 我们会说一个线程安全, 然后一个线程不安全啊, 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 表做操作,这就保证了线程安全.

被折叠的 条评论
为什么被折叠?



