.:
前言
Map 这样的 Key Value 在软件开发中是非常经典的结构,常用于在内存中存放数据。
本篇主要想讨论 ConcurrentHashMap 这样一个并发容器,在正式开始之前我觉得有必要谈谈 HashMap,没有它就不会有后面的 ConcurrentHashMap。
HashMap
众所周知 HashMap 底层是基于 数组 + 链表 组成的,不过在 jdk1.7 和 1.8 中具体实现稍有不同。
Base 1.7
1.7 中的数据结构图:
.:
先来看看 1.7 中的实现。
.:
这是 HashMap 中比较核心的几个成员变量;看看分别是什么意思?
初始化桶大小,因为底层是数组,所以这是数组默认的大小。
桶最大值。
默认的负载因子(0.75)
table 真正存放数据的数组。
Map 存放数量的大小。
桶大小,可在初始化时显式指定。
负载因子,可在初始化时显式指定。
重点解释下负载因子:
由于给定的 HashMap 的容量大小是固定的,比如默认初始化:
public HashMap() {
this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);
}
public HashMap(int initialCapacity, float loadFactor) {
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
this.loadFactor = loadFactor;
threshold = initialCapacity;
init();
}给定的默认容量为 16,负载因子为 0.75。Map 在使用过程中不断的往里面存放数据,当数量达到了 16 * 0.75 = 12 就需要将当前 16 的容量进行扩容,而扩容这个过程涉及到 rehash、复制数据等操作,所以非常消耗性能。
因此通常建议能提前预估 HashMap 的大小最好,尽量的减少扩容带来的性能损耗。
根据代码可以看到其实真正存放数据的是
transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;
这个数组,那么它又是如何定义的呢?
.:
Entry 是 HashMap 中的一个内部类,从他的成员变量很容易看出:
key 就是写入时的键。
value 自然就是值。
开始的时候就提到 HashMap 是由数组和链表组成,所以这个 next 就是用于实现链表结构。
hash 存放的是当前 key 的 hashcode。
知晓了基本结构,那来看看其中重要的写入、获取函数:
put 方法
public V put(K key, V value) {
if (table == EMPTY_TABLE) {
inflateTable(threshold);
}
if (key == null)
return putForNullKey(value);
int hash = hash(key);
int i = indexFor(hash, table.length);
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
Object k;
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
modCount++;