JDK7版本的源码解析
HashMap类是通过数组+链表的形式实现的
先来认识几个东东
类重要的属性
哈希表数组,长度必须为2的n次方
transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;
已经包含的元素个数
transient int size;
阈值,表示达到的元素个数需要扩容,threshold=capacity * load factor
int threshold;
负载因子
final float loadFactor;
哈希表中链表对象-重要的key-value对象
static class Entry<K,V> implements Map.Entry<K,V> {
final K key;
V value;
Entry<K,V> next;//链表的下一个对象
int hash;
Entry(int h, K k, V v, Entry<K,V> n) {
value = v;
next = n;
key = k;
hash = h;
}
public final int hashCode() {
//两个对象分别取hash然后异或操作得到该key-value对象的hash值
return Objects.hashCode(getKey()) ^ Objects.hashCode(getValue());
}
public final String toString() {
//如果打印的时候,是以key=value形式打印出来的
return getKey() + "=" + getValue();
}
...省略部分源码
}
最复杂的构造器
看一个最全的构造器,其它重载的构造器就不写了,无非是默认了方法参数,一看便知
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();//空方法,忽略
}
public V put(K key, V value)
看了上面这个构造器,是不是很奇怪,为什么没有初始化table变量呢?其实是在put方法里面初始化了
public V put(K key, V value) {
if (table == EMPTY_TABLE) {//第一次来,那肯定是满足空的条件了
inflateTable(threshold);//通过阈值参数去初始化table数组
}
if (key == null)//key为空的时候
return putForNullKey(value);
int hash = hash(key);//获取hashCode
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))) {
//e.hash通过addEntry源码发现就是key的hash值
//key的哈希值一样,且key相等则说明链表中已经存在同样的key,需要将旧value更新为新value
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
modCount++;
//如果table[i]=null或者在链表中没有找到对应的key,那么就添加一个key-value对象到table[i]位置的链表处
addEntry(hash, key, value, i);
return null;
}
初始化table数组
private void inflateTable(int toSize) {
// Find a power of 2 >= toSize
//传进来的是threshold,计算出最近的2的n次方的值
int capacity = roundUpToPowerOf2(toSize);
//threshold通过capacity * loadFactor计算出真正的值
threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
//创建table数组,默认初始化为null
table = new Entry[capacity];
initHashSeedAsNeeded(capacity);
}
如何计算2的n次方的值的呢?
private static int roundUpToPowerOf2(int number) {
// assert number >= 0 : "number must be non-negative";
return number >= MAXIMUM_CAPACITY
? MAXIMUM_CAPACITY
: (number > 1) ? Integer.highestOneBit((number - 1) << 1) : 1;
//借助Integer.highestOneBit求出整数对应二进制最高位为1的对应的十进制数
//number为什么不直接左移一位,而是要减1在左移呢?可以用8(1000)和9(1001)手动算一下Integer.highestOneBit((number - 1) << 1)就明白了
}
添加key=null元素
当put的key=null的时候,处理方式有点不一样
private V putForNullKey(V value) {
//找到数组第一个位置,并迭代从这个位置开始的链表,如果找到了key=null的,则修改值为新value,并返回旧value;如果数组第一个位置为null,则添加一个key=null,值为value的键值对,并返回null
for (Entry<K,V> e = table[0]; e != null; e = e.next) {
if (e.key == null) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
modCount++;
addEntry(0, null, value, 0);//当table[0]==null的时候执行
return null;
}
添加一条key-value到指定数组的位置
void addEntry(int hash, K key, V value, int bucketIndex) {
if ((size >= threshold) && (null != table[bucketIndex])) {
//在元素个数大于等于阈值,并且指定的数组位置不为null的时候进行扩容
//这个放到后面解析
resize(2 * table.length);
hash = (null != key) ? hash(key) : 0;
bucketIndex = indexFor(hash, table.length);
}
//在指定的数组位置添加一个key-value对象
createEntry(hash, key, value, bucketIndex);
}
void createEntry(int hash, K key, V value, int bucketIndex) {
//链表头插法,这个很好理解了吧,略
Entry<K,V> e = table[bucketIndex];
table[bucketIndex] = new Entry<>(hash, key, value, e);
size++;
}
根据key的hashCode和数组的容量计算出数组位置
static int indexFor(int h, int length) {
// assert Integer.bitCount(length) == 1 : "length must be a non-zero power of 2";
//求余h%(length-1)的高级写法
return h & (length-1);
}
数组扩容
现在重新看下addEntry方法的实现
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);
}
数组table扩容为原先2倍大小
void resize(int newCapacity) {
Entry[] oldTable = table;
int oldCapacity = oldTable.length;
if (oldCapacity == MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return;
}
//新建数组,newcapacity=2 * table.length,扩大为原数组大小的一倍
Entry[] newTable = new Entry[newCapacity];
//重点来了,执行扩容操作
transfer(newTable, initHashSeedAsNeeded(newCapacity));
//table指向扩容后的数组,即用新数组替换旧数组
table = newTable;
//重新计算阈值
threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
}
真正的扩容,也是头插的方式
void transfer(Entry[] newTable, boolean rehash) {
int newCapacity = newTable.length;
for (Entry<K,V> e : table) {//迭代每个数组位置
while(null != e) {//迭代每个数组指向的链表
//当前k-v对象指向的下一个对象引用临时保存到next
Entry<K,V> next = e.next;
//忽略
if (rehash) {
e.hash = null == e.key ? 0 : hash(e.key);
}
//计算在扩容后新数组中位置
int i = indexFor(e.hash, newCapacity);
//当前k-v对象指向下一个k-v对象,newTable[i]如果为null,则指向null;否则指向k-v对象
e.next = newTable[i];
//赋值操作,这个时候e引用指向的是newTable[i]
newTable[i] = e;
//移动引用指针e到之前保存到临时变量的下一个节点
e = next;
}
}
}
public V get(Object key)
接下来解析get方法,这个就相当简单了
public V get(Object key) {
//key=null时候从table[0]查询
if (key == null)
return getForNullKey();
//key!=null 查询
Entry<K,V> entry = getEntry(key);
return null == entry ? null : entry.getValue();
}
查询key=null的value
private V getForNullKey() {
if (size == 0) {
return null;
}
//迭代table[0]位置的链表
for (Entry<K,V> e = table[0]; e != null; e = e.next) {
if (e.key == null)
return e.value;
}
return null;
}
查询key!=null的value
final Entry<K,V> getEntry(Object key) {
if (size == 0) {
return null;
}
int hash = (key == null) ? 0 : hash(key);
for (Entry<K,V> e = table[indexFor(hash, table.length)];
e != null;
e = e.next) {//通过key哈希值定位到数组位置,并迭代该数组表示的链表
Object k;
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
//找到了key直接返回
return e;
}
return null;
}
线程安全问题
温习关键步骤
从上面分析可知,在put方法里面有个扩容的操作transfer。现在抽出几个关步骤:
for (Entry<K,V> e : table) {
while(null != e) {
Entry<K,V> next = e.next;
int i = indexFor(e.hash, newCapacity);
e.next = newTable[i];
newTable[i] = e;
e = next;
}
}
分析一种导致线程安全情况
导致线程安全的情况不限于此。
假设现在有两个线程T1和T2,同时将table[i]的第一个链表元素重新哈希到新数组的位置(多线程操作共享资源形成了竞争关系)
分析1: T1已经执行到newTable[i] = e;那么此时n.next=null(因为刚开始newTable[i]是null);线程T1停下来了;
分析2: T2开始从Entry<K,V> next = e.next;执行,那么由于T1的执行,此时T2线程执行完Entry<K,V> next = e.next后,next=null,在执行e.next = newTable[i];则e.next指向了自己e,形成了循环;在执行e = next;的时候e==null,while将退出,并开始下一个for循环
从上面可知,在扩容后会出现循环链表的情况,那么在结合之前分析get方法,可知在get中for循环的条件一直满足,但是if条件一直不满足的情况下,则出现了查询死循环,get方法一直得不到返回,有可能导致CPU 100%
容量必须是2的n次方大小?
是的,是的
两点原因
- 让元素尽可能平均分布,减少哈希碰撞
- 利用二进制计算效率。在2的n次方的前提下,h%(length-1)与h & (length-1)会相等,而且按位与的效率更高