1.首先是从hashmap中put元素开始分析
HashMap<String, String> map = new HashMap<>();
map.put("123", "1");
map.get("123");
2.点击put方法进入到里面
public V put(K key, V value) {
// 初始化动作
if (table == EMPTY_TABLE) {
inflateTable(threshold);
}
// 插入key为null的判断
if (key == null)
return putForNullKey(value);
// 计算key在hashmap中数组的对应的下标
int hash = hash(key);
int i = indexFor(hash, table.length);
// 遍历map找到和当前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;
}
}
modCount++;
// 增加一个entry
addEntry(hash, key, value, i);
return null;
}
3.点击addEntry进入到里面去
void addEntry(int hash, K key, V value, int bucketIndex) {
// 判断实际大小是否超过了阈值,并且数组上对应下标的值不为null,则扩容为原来2倍
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);
}
4.先进入到继续添加entry的方法中来,点击createEntry
void createEntry(int hash, K key, V value, int bucketIndex) {
// e的引用指向对应数组下标上的值
Entry<K,V> e = table[bucketIndex];
// 创建一个新的entry,然后对应的next指向上面那个数组下标的值,最后将数组的引用改为新创建的entry的引用,实现了头插法,并向下移动了元素
table[bucketIndex] = new Entry<>(hash, key, value, e);
// hashmap的实际容量加1
size++;
}
对应的效果图如下,其中假设数组长度为4,对应下标为2,在单线程下移动的步骤如下:
第一步,e的引用指向对应数组下标上的值:
第二步,创建一个新的entry,然后对应的next指向上面那个数组下标的值:
第三步,将数组的引用改为新创建的entry的引用,实现了头插法,并向下移动了元素
5.我们再回来看hashmap中的resize方法
void resize(int newCapacity) {
Entry[] oldTable = table;
int oldCapacity = oldTable.length;
if (oldCapacity == MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return;
}
// 创建一个新的数组
Entry[] newTable = new Entry[newCapacity];
// 这里就是扩容之后的元素转移到新数组中
transfer(newTable, initHashSeedAsNeeded(newCapacity));
// 将新的数组赋值给当前table
table = newTable;
// 重新计算阈值
threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
}
6.继续点击transfer方法,进入到里面来
void transfer(Entry[] newTable, boolean rehash) {
int newCapacity = newTable.length;
// 遍历数组
for (Entry<K,V> e : table) {
// 这里假设指向数组下标为2的元素
while(null != e) {
// 然后指向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;
}
}
}
上面这个代码的逻辑是,扩容之后转移元素到新的数组上,下面是在单线程下的情况扩容
第一步,遍历数组,指向数组上元素,然后next指向e的下一个
第二步,扩容数组长度为6,重新计算hash之后,对应数组下标有两种情况一种是还是之前位置,另外一种是原来位置的长度加上原来数组的长度,例如,之前下标是2,扩容之后下标为2或者5
下面这个图对应e.next = newTable[i];这一段代码逻辑
第三步,头插法插入元素,并下移元素,对应newTable[i] = e;这一段代码逻辑
第四步,e重新指向next元素,对应e = next;这一段代码逻辑
第五步,以此类推,完成了对原数组的扩容之后的转移,头插法之后元素的顺序会颠倒到来
7.而在多线程情况下就会出现循环链表,这里假设一个线程已经完成了元素转移,但是另外一个线程在只执行到transfer方法的newTable[i] = e;这一段代码的时候被挂起了,在它重新获得cpu执行时间片的时候,再进行转移就会出现循环链表,这里是由于jdk1.7下put的时候采用的是头插法,转移之后会对原有的元素顺序反转,所以就会出现循环链表
8.并且在多线程下,还会出现数据丢失问题,同样是在上面那种情况下一个执行完成,另外一个挂起之后再执行,就会出现下面图的情况,转移之后原来的数据3丢失了