HashMap源码
简介
HashMap
是一种基于哈希表的 Map 接口实现,它存储键值对(key-value pairs),并允许使用键来快速检索值。在 Java 中,HashMap
是 java.util
包的一部分,它不是同步的,这意味着它不是线程安全的。如果你需要线程安全的版本,可以使用 ConcurrentHashMap
。
以下是 HashMap
的一些关键特性:
- 允许空键和空值:
HashMap
允许键和值为null
。 - 非同步:
HashMap
不是线程安全的。如果你需要线程安全的HashMap
,可以使用Collections.synchronizedMap
方法包装一个HashMap
,或者使用ConcurrentHashMap
。 - 迭代顺序:从 JDK 1.8 开始,
HashMap
保证在并发访问的情况下,迭代顺序是可预测的,即按照插入顺序进行迭代。 - 初始容量和加载因子:
HashMap
可以指定初始容量和加载因子来优化性能。初始容量是哈希表中桶的数量,加载因子是一个影响哈希表扩展的阈值。 - 哈希冲突:当两个键的哈希码相同,或者哈希码经过数组索引计算后位置相同时,会发生哈希冲突。
HashMap
使用链表(在 JDK 1.8 之后是链表和红黑树的结合)来解决冲突。
以下是 Java 中 HashMap
的一个简单示例:
import java.util.HashMap;
public class HashMapExample {
public static void main(String[] args) {
HashMap<String, Integer> map = new HashMap<>();
map.put("apple", 1);
map.put("banana", 2);
map.put("cherry", 3);
// 获取值
Integer cherryValue = map.get("cherry");
System.out.println("Cherry value: " + cherryValue);
// 检查键是否存在
if (map.containsKey("banana")) {
System.out.println("Banana is in the map.");
}
// 删除键值对
map.remove("apple");
// 遍历 HashMap
for (Map.Entry<String, Integer> entry : map.entrySet()) {
System.out.println(entry.getKey() + " : " + entry.getValue());
}
}
}
在这个示例中,我们创建了一个 HashMap
实例,并添加了一些键值对。然后我们获取了一个值,检查了一个键是否存在,删除了一个键值对,并遍历了 HashMap
。
源码
hashmap成员变量
//创建 HashMap 时未指定初始容量情况下的默认容量,也就是16
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
//HashMap 的最大容量,也就是2的30次方=1 073 741 824
static final int MAXIMUM_CAPACITY = 1 << 30;
//HashMap 默认的装载因子,当 HashMap 中元素数量超过 容量*装载因子 时,进行resize()操作,也就是扩容
static final float DEFAULT_LOAD_FACTOR = 0.75f;
//用来确定何时将解决 hash 冲突的链表转变为红黑树
static final int TREEIFY_THRESHOLD = 8;
// 用来确定何时将解决 hash 冲突的红黑树转变为链表
static final int UNTREEIFY_THRESHOLD = 6;
//当需要将解决 hash 冲突的链表转变为红黑树时,需要判断下此时数组容量,若是由于数组容量太小(小于MIN_TREEIFY_CAPACITY)导致的hash冲突太多,则不进行链表转变为红黑树操作,会先利用resize()函数对hashMap扩容
static final int MIN_TREEIFY_CAPACITY = 64;
// 这个就是hashMap的内部数组了,而Node则是链表节点对象。
transient Node<K,V>[] table;
// 数组扩容阈值。
int threshold;
hashmap的节点的数据结构
static class Node<K,V> implements Map.Entry<K,V> {
//存储了键对象的哈希码,用于快速定位桶的位置
final int hash;
//存储键对象
final K key;
//存储与键相关联的值
V value;
//指向下一个节点的引用,用于解决哈希冲突,当链表长度好过一定阈值,列表会转成红黑树,以提高搜索效率
Node<K,V> next;
Node(int hash, K key, V value, Node<K,V> next) {
this.hash = hash;
this.key = key;
this.value = value;
this.next = next;
}
public final K getKey() { return key; }
public final V getValue() { return value; }
public final String toString() { return key + "=" + value; }
public final int hashCode() {
return Objects.hashCode(key) ^ Objects.hashCode(value);
}
public final V setValue(V newValue) {
V oldValue = value;
value = newValue;
return oldValue;
}
public final boolean equals(Object o) {
if (o == this)
return true;
if (o instanceof Map.Entry) {
Map.Entry<?,?> e = (Map.Entry<?,?>)o;
if (Objects.equals(key, e.getKey()) &&
Objects.equals(value, e.getValue()))
return true;
}
return false;
}
}
hashmap的put方法
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
/**
* Implements Map.put and related methods.
*
* @param hash hash for key
* @param key the key
* @param value the value to put
* @param onlyIfAbsent if true, don't change existing value 当存入键值对时,如果该key已存在,是否覆盖它的value。false为覆盖,true为不覆盖
* @param evict if false, the table is in creation mode. 用于子类
* @return previous value, or null if none
*/
//
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab;//内部数组
Node<K,V> p; //hash对应的索引中的首节点
int n, i;//n 内部数组的长度 i hash对应的索引位
//首次put时,内部数组为空,扩充数组
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
//计算数组索引,计算该索引位置的首节点,如果为null,添加一个新节点
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e; K k;
//如果首节点的key和要存入的key相同,那么直接覆盖value值
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
//如果首节点是红黑树,将键值对插入到红黑树中
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
for (int binCount = 0; ; ++binCount) {
//到达链表尾部,
if ((e = p.next) == null) {
//向链表尾部插入新节点
p.next = newNode(hash, key, value, null);
//判断链表元素>=8-1 ? 尝试转换红黑树 : 不转换
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
//转换之前会先判断当前数组容量是否>=64 ? 转换红黑树 : resize扩容
treeifyBin(tab, hash);
break;
}
//检查链表中是否已包含key
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
//如果key存在则覆盖value
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
//fail-fast机制
// Java集合中的一种错误检测机制,当多个线程对集合进行结构性的改变时,有可能会出发fail-fast机制,这个时候会抛出ConcurrentModificationException异常。
++modCount;//集合实际被修改的次数
//如果集合容量大于阈值,则进行扩容
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
hashmap的resize扩容操作
final Node<K,V>[] resize() {
//旧的数组 hashmap的内部数组
Node<K,V>[] oldTab = table;
//旧的数组长度 如果数组长度为空=0,不为空则是数组的长度
int oldCap = (oldTab == null) ? 0 : oldTab.length;
//旧的扩容阈值
int oldThr = threshold;
int newCap, newThr = 0;
//旧的数组长度大于0
if (oldCap > 0) {
//旧的数组长度是否大于等于hashmap的最大容量
if (oldCap >= MAXIMUM_CAPACITY) {
//扩容阈值就是 int的最大值2的31次方-1
threshold = Integer.MAX_VALUE;
//此时已经是最大值了,不扩容直接返回旧数组
return oldTab;
}
//新数组长度=2*旧数组长度 小于hashmap的最大容量 且旧数组长度大于默认值16 13644391557
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // double threshold
}
//旧数组长度等于0 且旧的阈值大于0 新的初始数组容量被置为旧的阈值
else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;
else { // zero initial threshold signifies using defaults
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
if (newThr == 0) {
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
threshold = newThr;
//创建一个新的链表
@SuppressWarnings({"rawtypes","unchecked"})
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;
if (oldTab != null) {
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
if ((e = oldTab[j]) != null) {
oldTab[j] = null;
//处理旧链表尾节点
if (e.next == null)
newTab[e.hash & (newCap - 1)] = e;
//如果是红黑树,
else if (e instanceof TreeNode)
//将一个红黑树节点分裂成两个子树。这个过程通常发生在并发环境下,当哈希表的大小超过某个阈值时,会将链表转换为红黑树以提高搜索效率。
// 这里的 split 方法就是将一个红黑树节点分裂成两个子树,并将它们重新链接到哈希表的桶中。
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
else { // preserve order
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
//根据节点的哈希值与旧容量 oldCap 的按位与操作的结果将它们分配到两个新的链表中。e.hash & oldCap 的结果决定了节点应该属于哪个新链表。
do {
next = e.next;
if ((e.hash & oldCap) == 0) {
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
else {
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
hashmap的get方法
public V get(Object key) {
Node<K,V> e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
get方法查找对应节点
final Node<K,V> getNode(int hash, Object key) {
Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
//判断容器是否为空
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {
//查看哈希值是否与首节点的哈希值相同
if (first.hash == hash && // always check first node
((k = first.key) == key || (key != null && key.equals(k))))
return first;
//首节点不是目标节点且首节点的下一个节点不为空
if ((e = first.next) != null) {
//红黑树结构查找节点
if (first instanceof TreeNode)
return ((TreeNode<K,V>)first).getTreeNode(hash, key);
do {
//链表结构查找节点
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
return null;
}
HashMap中的死锁
HashMap会造成死锁,因为HashMap是线程非安全的,并发的情况容易造成死锁,若要高并发推荐使用ConcurrentHashMap,这里的加了锁。
我们假设有二个进程T1、T2,HashMap容量为2,T1线程放入key A、B、C、D、E。在T1线程中A、B、C
Hash值相同,于是形成一个链,假设为A->C->B,而D、E
Hash值不同,于是容量不足,需要新建一个更大尺寸的hash表,然后把数据从老的Hash表中
迁移到新的Hash表中(refresh)。这时T2进程闯进来了,T1暂时挂起,T2进程也准备放入新的key,这时也
发现容量不足,也refresh一把。refresh之后原来的链表结构假设为C->A,之后T1进程继续执行,链接结构
为A->C,这时就形成A.next=B,B.next=A的环形链表。一旦取值进入这个环形链表就会陷入死循环。
死锁是指两个或多个线程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法向前推进。
HashMap 可能导致死锁的情况通常与以下因素有关:
重入:当一个线程在扩容(rehashing)过程中,如果另一个线程尝试插入元素,可能会触发新的扩容操作。如果多个线程同时进行这样的操作,它们可能会相互等待对方释放锁,从而导致死锁。
并发修改:如果多个线程同时尝试修改 HashMap 的结构(如插入或删除元素),而这些操作没有适当的同步控制,就可能导致死锁。
不当的迭代:在多线程环境中,如果一个线程在迭代 HashMap 的同时,另一个线程修改了 HashMap,可能会导致迭代器抛出 ConcurrentModificationException。虽然这通常不会导致死锁,但如果迭代器的实现不当,或者在处理异常时没有正确地同步,也可能间接导致死锁。
为了避免 HashMap 的死锁问题,可以采取以下措施:
使用 ConcurrentHashMap:这是 Java 提供的一个线程安全的 HashMap 实现。它通过分段锁(segmentation)来允许并发的读写操作,从而避免了死锁。
同步包装:可以使用 Collections.synchronizedMap 方法将 HashMap 包装为线程安全的 Map。但这种方式在高并发环境下性能可能不佳,因为它对整个 Map 加锁,而不是对单个元素。
显式锁:可以使用 ReentrantLock 或 synchronized 块来手动管理对 HashMap 的访问,确保在修改 HashMap 时只有一个线程能够进行操作。
不可变 Map:如果不需要修改 Map,可以使用 Collections.unmodifiableMap 创建一个不可修改的视图,这样即使在多线程环境下也不会出现死锁。
避免在循环中进行结构修改:在循环中进行 HashMap 的结构修改(如扩容)可能会导致死锁,应该避免这种情况。
使用读写锁:如果读操作远多于写操作,可以考虑使用读写锁(ReentrantReadWriteLock),它允许多个读线程同时访问,但写线程需要独占访问。