Map 这样的Key Value在软件开发中是很是经典的结构,经常使用于在内存中存放数据。数据结构
本篇主要想讨论 ConcurrentHashMap 这样一个并发容器,在正式开始以前我以为有必要谈谈 HashMap,没有它就不会有后面的 ConcurrentHashMap。并发
众所周知 HashMap 底层是基于数组 + 链表组成的,不过在 jdk1.7 和 1.8 中具体实现稍有不一样。函数
Base 1.7性能
1.7 中的数据结构图:this
先来看看 1.7 中的实现。spa
这是 HashMap 中比较核心的几个成员变量;看看分别是什么意思?code
初始化桶大小,由于底层是数组,因此这是数组默认的大小。对象
桶最大值。内存
默认的负载因子(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[] table = (Entry[]) 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 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++;
addEntry(hash, key, value, i);
return null;
}
判断当前数组是否须要初始化。
若是 key 为空,则 put 一个空值进去。
根据 key 计算出 hashcode。
根据计算出的 hashcode 定位出所在桶。
若是桶是一个链表则须要遍历判断里面的 hashcode、key 是否和传入 key 相等,若是相等则进行覆盖,并返回原来的值。
若是桶是空的,说明当前位置没有数据存入;新增一个 Entry 对象写入当前位置。
void addEntry(int hash, K key, V value, int bucketIndex) {
if ((size >= threshold) && (null != table[bucketIndex])) {
resize(2 * table.length);
hash = (null != key) ? hash(key) : 0;
bucketIndex = indexFor(hash, table.length);
}
createEntry(hash, key, value, bucketIndex);
}
void createEntry(int hash, K key, V value, int bucketIndex) {
Entry e = table[bucketIndex];
table[bucketIndex] = new Entry<>(hash, key, value, e);
size++;
}
当调用 addEntry 写入 Entry 时须要判断是否须要扩容。
若是须要就进行两倍扩充,并将当前的 key 从新 hash 并定位。
而在createEntry中会将当前位置的桶传入到新建的桶中,若是当前桶有值就会在位置造成链表。
再来看看 get 函数:
public V get(Object key) {
if (key == null)
return getForNullKey();
Entry entry = getEntry(key);
return null == entry ? null : entry.getValue();
}
final Entry getEntry(Object key) {
if (size == 0) {
return null;
}
int hash = (key == null) ? 0 : hash(key);
for (Entry e = table[indexFor(hash, table.length)];
e != null;
e = e.next) {
Object k;
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
}
return null;
}
首先也是根据 key 计算出 hashcode,而后定位到具体的桶中。
判断该位置是否为链表。
不是链表就根据key、key 的 hashcode是否相等来返回值。
为链表则须要遍历直到 key 及 hashcode 相等时候就返回值。
啥都没取到就直接返回 null 。
做者:AI乔治 连接:https://www.jianshu.com/p/7e36a15f7d3a 來源:简书 简书著做权归做者全部,任何形式的转载都请联系做者得到受权并注明出处。