HashMap:基于哈希表的 Map 接口的实现,并允许使用 null 值和 null 键。底层数据结构主要有数组和链表。数组存储区间连续,占用内存较多,寻址容易,插入和删除困难。链表存储区间离散,占用内存较少,寻址困难,插入和删除容易。HashMap的最小处理单元是Entry<K,V>,Entry 包含四个属性:key, value, hash 值和用于单向链表的 next。
ConcurrentHashMap: 底层数据结构是数组和链表,key、value不能为null。ConcurrentHashMap最外层是一个Segment的数组。每个Segment包含一个与HashMap数据结构差不多的链表数组。ConcurrentHashMap的最小存储单元是HashEntry<K,V>,包含四个属性key, value, hash 值和用于单向链表的 next。
先从方法上看一下两者的区别:
HashMap:
//构造函数
HashMap() 构造一个具有默认初始容量 (16) 和默认加载因子 (0.75) 的空 HashMap。
HashMap(int initialCapacity) 构造一个带指定初始容量和默认加载因子 (0.75) 的空 HashMap。
HashMap(int initialCapacity, float loadFactor) 构造一个带指定初始容量和加载因子的空 HashMap。
HashMap(Map<? extends K,? extends V> m) 构造一个映射关系与指定 Map 相同的新 HashMap。
//常用方法
void clear() 从此映射中移除所有映射关系。
V get(Object key) 返回指定键所映射的值;如果对于该键来说,此映射不包含任何映射关系,则返回 null。
V put(K key, V value) 在此映射中关联指定值与指定键。
V remove(Object key) 从此映射中移除指定键的映射关系(如果存在)。
boolean containsKey(Object key) 如果此映射包含对于指定键的映射关系,则返回 true。
boolean containsValue(Object value) 如果此映射将一个或多个键映射到指定值,则返回 true。
Set<K> keySet() 返回此映射中所包含的键的 Set 视图。
int size() 返回此映射中的键-值映射关系数。
boolean isEmpty() 如果此映射不包含键-值映射关系,则返回 true。
Set<Map.Entry<K,V>> entrySet() 返回此映射所包含的映射关系的 Set 视图。
void putAll(Map<? extends K,? extends V> m) 将指定映射的所有映射关系复制到此映射中,这些映射关系将替换此映射目前针对指定映射中所有键的所有映射关系。
Collection<V> values() 返回此映射所包含的值的 Collection 视图。
Object clone() 返回此 HashMap 实例的浅表副本:并不复制键和值本身。
ConcurrentHashMap:
//构造方法
ConcurrentHashMap(int initialCapacity, float loadFactor, int concurrencyLevel) 创建一个带有指定初始容量、加载因子和并发级别的新的空映射。
ConcurrentHashMap(int initialCapacity, float loadFactor) 创建一个带有指定初始容量、加载因子和默认 concurrencyLevel (16) 的新的空映射
ConcurrentHashMap(int initialCapacity) 创建一个带有指定初始容量、默认加载因子 (0.75) 和 concurrencyLevel(16) 的新的空映射
ConcurrentHashMap() 带有默认初始容量 (16)、加载因子 (0.75) 和 concurrencyLevel (16) 的新的空映射
ConcurrentHashMap(Map<? extends K, ? extends V> m) 构造一个与给定映射具有相同映射关系的新映射
//常用方法
void clear() 从该映射中移除所有映射关系
V putIfAbsent(K key, V value) 如果map中已有指定key,返回key对应的value,否则将指定的key-value放入map中。
V put(K key, V value) 将指定键映射到此表中的指定值。
V get(Object key) 返回指定键所映射到的值,如果此映射不包含该键的映射关系,则返回 null。
V remove(Object key) 从此映射中移除键(及其相应的值)。
boolean remove(Object key, Object value) 只有map中指定的key映射指定的value,才移除该键的条目
V replace(K key, V value) 只有map中包含指定的key,才将key映射为value值
boolean replace(K key, V oldValue, V newValue) 只有map中指定的key映射指定的value,才替换该键映射
boolean contains(Object value) 等同于containsValue(Object value)
boolean containsKey(Object key) 测试指定对象是否为此表中的键。
boolean containsValue(Object value) 如果此映射将一个或多个键映射到指定值,则返回 true。
int size() 返回此映射中的键-值映射关系数。
Enumeration<V> elements() 返回此表中值的枚举。
Set<Map.Entry<K,V>> entrySet() 返回此映射所包含的映射关系的 Set 视图。
boolean isEmpty() 如果此映射不包含键-值映射关系,则返回 true。
Enumeration<K> keys() 返回此表中键的枚举。
Set<K> keySet() 返回此映射中包含的键的 Set 视图。
void putAll(Map<? extends K,? extends V> m) 将指定映射中所有映射关系复制到此映射中。
Collection<V> values() 返回此映射中包含的值的 Collection 视图。
从两个类的方法中可以发现,HashMap对数据的处理比较简单,ConcurrentHashMap对数据的处理粒度更细,因为ConcurrentHashMap是线程安全的,所以它的操作比较复杂。
对比一下两个方法中部分同名的方法的源码:
put:
HashMap的put方法:
public V put(K key, V value) {
if (table == EMPTY_TABLE) { //static final Entry<?,?>[] EMPTY_TABLE = {};
inflateTable(threshold);
}
if (key == null)
return putForNullKey(value);
int hash = hash(key); //key的hash值
int i = indexFor(hash, table.length); //key对应的数组下标
//遍历链表,如果在遍历过程中,链表元素被其它线程修改,会造成线程安全问题
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
//遍历数组,如果key存在,更改key对应的value,并返回oldValue
Object k;
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this); //当put方法覆盖原有的key—value时调用
return oldValue;
}
}
modCount++;
//新增元素
addEntry(hash, key, value, i);
return null;
}
ConcurrentHashMap的put方法:
public V put(K key, V value) {
Segment<K,V> s;
if (value == null) //key、value不能为null
throw new NullPointerException();
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);
}
这里调用的是Segment的put方法:
final V put(K key, int hash, V value, boolean onlyIfAbsent) {
HashEntry<K,V> node = tryLock() ? null :
scanAndLockForPut(key, hash, value); //扫描包含给定key的节点,同时尝试获取锁,没有找到,则创建并返回一个节点。返回时,确保锁被持有。
V oldValue;
try {
HashEntry<K,V>[] tab = table;
int index = (tab.length - 1) & hash;
HashEntry<K,V> first = entryAt(tab, index);// first 是数组该位置处的链表的表头
for (HashEntry<K,V> e = first;;) {
if (e != null) {
K k;
if ((k = e.key) == key || (e.hash == hash && key.equals(k)))
{//原数组中存在key
oldValue = e.value;
if (!onlyIfAbsent) {//覆盖旧值
e.value = value;
++modCount;
}
break;
}
e = e.next; //获取链表的下一个节点
}
else {
if (node != null)
node.setNext(first); //不为 null,那就直接将它设置为链表表头
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;
}
Segment的put方法的finally中调用了unlock方法,进入put方法执行的第一条语句是:HashEntry<K,V> node = tryLock() ? null : scanAndLockForPut(key, hash, value),如果获取锁,则node节点为null,否则调用scanAndLockForPut方法。下面重点看看scanAndLockForPut中如何获取锁:
private HashEntry<K,V> scanAndLockForPut(K key, int hash, V value) {
HashEntry<K,V> first = entryForHash(this, hash);
HashEntry<K,V> e = first;
HashEntry<K,V> node = null;
int retries = -1; //自旋次数计数器
while (!tryLock()) {//没有获取到锁进入循环
HashEntry<K,V> f; // to recheck first below
if (retries < 0) {
if (e == null) {
if (node == null) // 链表为空,创建新节点
node = new HashEntry<K,V>(hash, key, value, null);
retries = 0;
}
else if (key.equals(e.key)) //key已存在
retries = 0;
else //key不存在,获取下一个节点
e = e.next;
}
//retries>=0,则node节点不为null
else if (++retries > MAX_SCAN_RETRIES) { //超过最大重试次数
lock();
break;
}
else if ((retries & 1) == 0 && //retries为偶数
//其它线程修改了链表中的头元素,重新执行循环(保证线程安全的关键)
(f = entryForHash(this, hash)) != first) {
e = first = f; // re-traverse if entry changed
retries = -1;
}
}
return node;
}
ConcurrentHashMap的put方法,的关键在其内部类Segment的put方法,该方法中对节点的操作分为创建新节点和修改原有节点。如果新增的key原链表中已存在,则修改原有节点为新value;若新增的key在原链表中不存在则新增节点,在put和scanAndLockForPut方法都有新建节点操作,put方法创建新节点前已经获取到锁标记,所以直接创建新节点并设置为链表的头元素;scanAndLockForPut方法创建新节点时,会尝试获取锁标记,并检查链表的头元素是否被改变,若果被改变,则重新执行循环。其实不论是put方法还是scanAndLockForPut方法,核心操作是获取锁标记之后再对数据进行操作,以保证数据的线程安全性。
remove:
HashMap的remove方法:
public V remove(Object key) {
Entry<K,V> e = removeEntryForKey(key);
return (e == null ? null : e.value);
}
final Entry<K,V> removeEntryForKey(Object key) {
if (size == 0) //链表为空,返回null
return null;
int hash = (key == null) ? 0 : hash(key);
int i = indexFor(hash, table.length);
Entry<K,V> prev = table[i];
Entry<K,V> e = prev;
while (e != null) {//遍历链表查找key
Entry<K,V> next = e.next;
Object k;
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k)))) {
modCount++;
size--;
if (prev == e)
table[i] = next;
else
prev.next = next;
e.recordRemoval(this); //移除当前节点
return e;
}
prev = e;
e = next;
}
return e;
}
HashMap的remove方法只是简单的查找key,并移除对应的节点。
ConcurrentHashMap的remove方法:
public V remove(Object key) {
int hash = hash(key);
Segment<K,V> s = segmentForHash(hash);
//调用segment的remove方法
return s == null ? null : s.remove(key, hash, null);
}
Segment的remove方法:
final V remove(Object key, int hash, Object value) {
//获取锁标记
if (!tryLock())
scanAndLock(key, hash); //与scanAndLockForPut相似,获取锁标记并返回。
V oldValue = null;
try {
HashEntry<K,V>[] tab = table;
int index = (tab.length - 1) & hash;
HashEntry<K,V> e = entryAt(tab, index);
HashEntry<K,V> pred = null;
while (e != null) { //遍历链表
K k;
HashEntry<K,V> next = e.next;
//查找到目标key
if ((k = e.key) == key || (e.hash == hash && key.equals(k))) {
V v = e.value;
//对value的判断
if (value == null || value == v || value.equals(v)) {
if (pred == null)
setEntryAt(tab, index, next); //设置index处的元素为e.next
else
pred.setNext(next);
++modCount;
--count;
oldValue = v;
}
break;
}
pred = e;
e = next;
}
} finally {
unlock();
}
return oldValue;
}
ConcurrentHashMap的remove方法与put方法的思想基本相同,先获取锁标记,保证线程安全,然后再去处理数据。
get方法:
HashMap的get方法:
public V get(Object key) {
if (key == null)
return getForNullKey();
Entry<K,V> entry = getEntry(key);
return null == entry ? null : entry.getValue();
}
ConcurrentHashMap的get方法:
public V get(Object key) {
Segment<K,V> s; // manually integrate access methods to reduce overhead
HashEntry<K,V>[] tab;
int h = hash(key);
//获取key所在的Segment的下标
long u = (((h >>> segmentShift) & segmentMask) << SSHIFT) + SBASE;
//从segments数组中取出包含key的Segment元素
if ((s = (Segment<K,V>)UNSAFE.getObjectVolatile(segments, u)) != null &&
(tab = s.table) != null) {
for (HashEntry<K,V> e = (HashEntry<K,V>) UNSAFE.getObjectVolatile
(tab, ((long)(((tab.length - 1) & h)) << TSHIFT) + TBASE);
e != null; e = e.next) {
K k;
if ((k = e.key) == key || (e.hash == h && key.equals(k)))
return e.value;
}
}
return null;
}
HashMap的get方法比较简单,遍历链表找到包含指定key的Entry对象,然后取出Entry的value属性值即为目标值;ConcurrentHashMap的get方法则是先根据Key找到segments数组中包含key的Segment对象,在从Segment对象的HashEntry数组中找到目标key所在的HashEntry对象,取出HashEntry对象的value属性值即为目标值。这里要注意的一点是:key的hash值与Segment数组中Segment下标的对应关系—— (((h >>> segmentShift) & segmentMask) << SSHIFT) + SBASE,在新增、修改、删除数据时使用的是同一个算法。