一. 概述
首先我们都知道java1.7中hashmap使用的数据结构是数组加链表;hashmap也不是线程安全的;它支持key和value都是null,但是这样的值只允许有一个。
hashMap的底层是用了一个Entry的数组,然后数组中的一个每一个节点对应着一个链表。
二. 源码解析
- 源码中重要变量
//默认初始化容量,1向左移4位,就是16
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
//最大容量;也就是2的30次方,这是因为int类型可以表示的最大数是2的32次方
static final int MAXIMUM_CAPACITY = 1 << 30;
//threshold的最大值
static final int ALTERNATIVE_HASHING_THRESHOLD_DEFAULT = Integer.MAX_VALUE;
//默认加载因子0.75,
static final float DEFAULT_LOAD_FACTOR = 0.75f;
//table用来存储Entry数组
transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;
//已经使用的数组的位置个数,用来判断是否需要扩容
transient int size;
//阈值,当table == {}时,该值为初始容量(初始容量默认为16);当table被填充了
//也就是为table分配内存空间后,threshold一般为 capacity*loadFactory。
//HashMap在进行扩容时需要参考threshold
int threshold;
//负载因子,表示table的填充度,默认0.75
final float loadFactor;
- put()方法
public V put(K key, V value) {
if (table == EMPTY_TABLE) {
// 真正的初始化方法
// 代码比较简单
// threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
// threshold 初始时为12
// table = new Entry[capacity];
// 最后单独说initHashSeedAsNeeded(capacity);
inflateTable(threshold); // 这里传的是初始时 的 16
}
// 为null值 指定位置
// table[0]的链表 并返回旧值或null
if (key == null)
return putForNullKey(value);
// 计算hash值
int hash = hash(key);
// 为利用hash找到 key应该放到table的某个数组的链下
int i = indexFor(hash, table.length);
// 遍历 table[i]链表 找到与当前key相同的替换并返回旧值
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;
}
}
// 若table[i]下没有相同的key 则 modCount+1,并执行addEntry将key,value放到map中
modCount++;
addEntry(hash, key, value, i);
return null;
}
执行addEntry方法将key,value存入map中。
此方法先计算是否需要扩容后真正放值。
void addEntry(int hash, K key, V value, int bucketIndex) {
// add之前会判断当前map中的数量是否大于等于阈值
// 扩容条件 当前size超过阈值 并且 table[bucketIndex] 不为空
if ((size >= threshold) && (null != table[bucketIndex])) {
// 扩容原先的两倍
resize(2 * table.length);
// 重新计算hash值
hash = (null != key) ? hash(key) : 0;
// 计算当前key所属的 table数组位置
bucketIndex = indexFor(hash, table.length);
}
createEntry(hash, key, value, bucketIndex);
}
jdk中数据的插入是先通过计算得到key的hashcode值,然后通过hashcode值与当前的桶值进行与运算(桶值就是现在的数组长度,然后与运算是这个hashcode的值与这个数组长度的值-1进行与运算,),然后插入到对应的桶中,一个桶对应的是一个链表,然后插入链表的时候是采用头节点插入,这样插入的速度更快。然后在桶中存放的是对应的链表的引用,然后在你插入之后将新的头节点的引用给桶就可以了
在put()数据的时候key是可以相同的,但是它是会覆盖key相同节点的value,并且返回
因为你在插入的时候都需要去遍历查看是否有相同的key,那为什么插入的时候要用头插法而不用尾插法?
因为你在使用尾插法的时候每次都要遍历到最后,但是使用头插法的时候不用每次遍历到最后。
3. get()方法
// 获取key对应的value
public V get(Object key) {
if (key == null)
//如果key为null,调用getForNullKey()
return getForNullKey();
//key不为null,调用getEntry(key);
Entry<K,V> entry = getEntry(key);
return null == entry ? null : entry.getValue();
}
//当key为null时,获取value
private V getForNullKey() {
if (size == 0) {
return null;//链表为空,返回null
}
//链表不为空,将“key为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) {//判断链表中是否有值
//链表中没值,也就是没有value
return null;
}
//链表中有值,获取key的hash值
int hash = (key == null) ? 0 : hash(key);
// 在“该hash值对应的链表”上查找“键值等于key”的元素
for (Entry<K,V> e = table[indexFor(hash, table.length)];
e != null;
e = e.next) {
Object k;
//判断key是否相同
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;//key相等,返回相应的value
}
return null;//链表中没有相应的key
}
- resize()方法,扩容
扩容方法是创建一个新的Entry数组(容量为原先的两倍)
并将原先数组中的值转移到新数组中
void resize(int newCapacity) {
Entry[] oldTable = table;
int oldCapacity = oldTable.length;
// 如果当前table容量已经等于最大容量值则将阈值设定为整数最大值并返回
if (oldCapacity == MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;//2<sup>31</sup>-1
return;
}
Entry[] newTable = new Entry[newCapacity];
// 转移值 initHashSeedAsNeeded用于返回是否需要重新计算hash
transfer(newTable, initHashSeedAsNeeded(newCapacity));
// 转移值之后 更改table的引用
table = newTable;
// 更新阈值
threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
}
转移数组方法 transfer()方法
遍历table 并 循环遍历每个链表 为链表中每个元素计算隶属与数组哪个位置然后转移
void transfer(Entry[] newTable, boolean rehash) {
int newCapacity = newTable.length;
for (Entry<K,V> e : table) {
while(null != e) {
Entry<K,V> next = e.next;
if (rehash) {
e.hash = null == e.key ? 0 : hash(e.key);
}
int i = indexFor(e.hash, newCapacity);
e.next = newTable[i];
newTable[i] = e;
e = next;
}
}
}
扩容:扩容的时候是生成一个新的数组,然后通过遍历数组,在每个数组节点中对应的链表中取每一个节点元素的hash值,然后通过这个新的hash值和数组长度进行与运算,来算出它的数组下标。
在扩容的时候也是用的头插法
扩容的目的:其实扩容是为了将每个数组节点下面的链表变短,然后这样可以提高查询的效率。
它是线程不安全的,在多个线程使用同一个hashmap的时候可能会造成一个循环链表。
三. 为什么hashMap线程不安全
1. 扩容造成死循环
造成死循环的主要源头就是transfer()方法中,因为它使用的是头插法,所以说链表的顺序是会翻转的,这也是造成死循环的关键。
int newCapacity = newTable.length;
for (Entry<K,V> e : table) {
while(null != e) {
Entry<K,V> next = e.next;
if (rehash) {
e.hash = null == e.key ? 0 : hash(e.key);
}
int i = indexFor(e.hash, newCapacity);
e.next = newTable[i];
newTable[i] = e;
e = next;
}
}
假如这个时候有两个线程对同一个hashmap进行操作,然后线程A先开始执行,执行到newTable[i] =
e这里的时候停止。然后线程B执行,线程B全部执行完毕,然后执行的结果是正确的一个过程;这个时候线程A开始执行,因为这个时候线程B执行完毕,所以newTable和table中的Entry都是主存中的最新值,所以此时线程A都是用的是内存中的值。
此时如果有一个 next是执行线程A中的e,然后此时它在继续循环,到最后就会出现死循环,因为这个时候会出现e.next = e,next.next = e的情况,并且e =null。
- 扩容造成数据丢失
在执行put()操作的时候,这个也是同时有两个线程;
线程A先执行,在执行到newTable[i] = e的时候挂起,同时这个时候线程B获得时间片,然后完成resize()操作,同时支持完毕,这个时候newtable和table都是最新的值,再切换到线程A的时候,next指向的就会为null,执行 newtable[i] = e操作的时候就将本来是另一个元素存放的位置改成了另一个元素,再循环一次就会导致有元素丢失,并且线程循环链表,并造成死循环。