HashMap1.8剖析
1.HashMap1.8原型图
总结
从原型图上我们可以说看出,HashMap1.8的数据结构是:数组+链表+红黑树
这里我们想为什么要引入红黑树呢?
红黑树是平衡树可以提高hashmap的检索效率(时间复杂度从O(n)->O(logn))
2.我们来看作者对HashMap1.8的阐述
/**
* 基于哈希表的<tt>Map</tt>接口的实现。 这个
* 实现提供了所有可选的地图操作,并允许
<tt>null</tt>值和<tt>null</tt>键。 (<tt>HashMap</tt>类大致相当于<tt>HashMap</tt>。
* 类大致等同于<tt>Hashtable</tt>,除了它是
* 非同步化并且允许空值)。 这个类不保证
* 地图的顺序;特别是,它不保证该顺序
* 将随着时间的推移保持不变。
*
<p>这个实现为基本的*操作提供了恒定的时间性能。
* 操作(<tt>get</tt>和<tt>put</tt>)提供恒定的时间性能,假设哈希函数
* 适当地将元素分散到桶中。 迭代
* 的 "容量 "成正比的时间。
<tt>HashMap</tt>实例的 "容量"(桶的数量)加上其大小(键值映射的数量
* 键值映射的数量)。 因此,很重要的一点是,不要把初始的
* 如果迭代性能很重要的话,不要把初始容量设置得太高(或者负载系数太低)。
* 重要。
*
<p>一个<tt>HashMap</tt>的实例有两个参数影响其
* 性能。<i>初始容量</i>和<i>负载因子</i>。 其中
<i>容量</i>是哈希表中的桶的数量,而最初的
* 容量是创建哈希表时的容量。 负载系数
<i>负载因子</i>是一个衡量哈希表在自动增加容量之前被允许的满载程度的标准。
* 在其容量被自动增加之前,允许哈希表达到多满的程度。 当哈希表的条目数
* 哈希表中的条目数超过了负载因子和当前容量的乘积,哈希表就会被自动增加。
* 当前的容量,哈希表会被<i>重新洗牌</i>(也就是说,内部数据
* 结构被重建),从而使哈希表拥有大约两倍的
* 桶的数量。
*
* <p>作为一般规则,默认的负载因子(0.75)提供了一个良好的
* 时间和空间成本之间的权衡。 更高的值会减少
* 空间开销,但增加了查找成本(反映在大多数的
<tt>HashMap</tt>类的大部分操作,包括
<tt>get</tt>和<tt>put</tt>)。 地图中的预期条目数
* 在设置地图的初始容量时,应该考虑到地图的预期条目数和它的负载系数。
* 设置它的初始容量,以尽量减少
* 重洗操作。 如果初始容量大于
* 最大条目数除以负载系数,就不会发生重洗
* 操作将永远不会发生。
*
<p>如果许多映射要存储在一个<tt>HashMap</tt>实例中,以足够大的容量来创建它,就可以让它有足够的容量。
* 实例中,用足够大的容量来创建它将允许
* 比让它执行更有效的映射存储
* 在需要时自动重新洗牌以增加表。 请注意,使用
* 许多具有相同{@code hashCode()}的键是一个肯定的方式来减缓
* 任何哈希表的性能。为了减轻影响,当键值
* 是{@link Comparable}的时候,这个类可以使用键之间的比较顺序来帮助打破联系。
*键来帮助打破联系。
*
* <p><strong>注意,这个实现是不同步的。
* 如果多个线程同时访问一个哈希图,并且其中至少有一个
* 如果多个线程同时访问一个哈希图,并且至少有一个线程在结构上修改了该哈希图,那么它必须<i></i>被外部同步。
* 在外部进行同步。 (结构性修改是指任何操作
* 增加或删除一个或多个映射的操作;仅仅是改变与一个实例的键相关的值
* 仅仅改变与一个实例已经包含的键相关的值并不是
* 结构性修改)。 这通常是通过以下方式完成的
* 在一些自然封装了地图的对象上进行同步。
*
* 如果没有这样的对象存在,那么地图应该被 "包裹 "起来,使用
* {@link Collections#synchronizedMap Collections.synchronizedMap} * 方法。
* 方法进行 "包装"。 这最好在创建时完成,以防止意外的
* 对地图的非同步访问:<pre> * 地图m = Collections.synchronizedMap * 方法
* Map m = Collections.synchronizedMap(new HashMap(...));</pre>
*
<p>这个类的所有 "集合视图方法 "所返回的迭代器
* 是<i>快速失败的</i>:如果地图在创建后的任何时候被结构性地修改了
* 迭代器被创建后,除了通过迭代器本身的
<tt>remove</tt>方法,该迭代器将抛出一个
* {@link ConcurrentModificationException}。 因此,在面对并发的
* 修改时,迭代器会快速而干净地失败,而不是冒着
*任意的、非决定性的行为,在一个不确定的时间里。
* 未来。
*
<p>注意,迭代器的快速失效行为不能被保证。
* 因为一般来说,在非同步并发的情况下,不可能做出任何硬性保证。
* 存在非同步的并发修改。 失败快速的迭代器
* 抛出<tt>ConcurrentModificationException</tt>是在尽力而为的基础上。
* 因此,编写一个依赖这个异常的程序是错误的。
* 异常来保证其正确性。<i>迭代器的故障快速行为
* 应该只用于检测错误。
*
* <p>这个类是属于
* <a href="{@docRoot}/.../technotes/guides/collections/index.html">。
* Java集合框架</a>。
*
* @param <K> 这个映射所维护的键的类型
* @param <V> 类型
*/
从这里我们可以得到几个重要的信息
hashmap是允许null值null键的
这里说数组的容量其实就是桶的数量
如果迭代性能很重要的话,不要把初始容量设置得太高(或者负载系数太低)。
负载因子:是一个衡量哈希表在自动增加容量之前被允许的满载程度的标准。在其容量被自动增加之前,允许哈希表达到多满的程度。 当哈希表的条目数哈希表中的条目数超过了负载因子和当前容量的乘积,哈希表就会被自动增加。当前的容量,哈希表会被重新洗牌(也就是说,内部数据结构被重建),从而使哈希表拥有大约两倍的桶的数量。
默认的负载因子(0.75)提供了一个良好的时间和空间成本之间的权衡。
更高的值会减少空间开销,但增加了查找成本
hashmap是非线程安全的,如果想变成线程安全的可以转化一下,使用
Map m = Collections.synchronizedMap(new HashMap(...))方法
3.HashMap1.8中定义的常量
/**
*默认的初始容量 - 必须是2的幂。
*这里可以看出默认容量是16
*/
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
/**
* 最大容量,如果一个更高的值被隐含地指定,则使用该值。
* 的任何一个构造函数所隐含的更高值。
*必须是2的幂<=1<<30.
*/
static final int MAXIMUM_CAPACITY = 1 << 30;
/**
* 当构造函数中没有指定时,使用的负载因子。
*/
static final float DEFAULT_LOAD_FACTOR = 0.75f;
/**
* 使用树形而非列表的bin计数阈值。
* bin的阈值。 当添加一个元素到一个至少有这么多节点的bin时,bin会被转换为树。
* 至少有这么多的节点时,Bin会被转换为树。这个值必须大于
* 大于2,并且至少应该是8,以符合树形删除的假设。
* 树的移除在缩减时转换为普通的Bins。
* 缩减。
*/
static final int TREEIFY_THRESHOLD = 8;
/**
*就是当红黑树上的值只有6个时,这时转为链表结构
*/
static final int UNTREEIFY_THRESHOLD = 6;
/**
*最小的表的容量,对于它来说,bin可以被树化。
* (否则,如果一个bin中的节点太多,表就会被调整大小)。
* 应该至少是4 * TREEIFY_THRESHOLD以避免冲突
* 调整大小和树化阈值之间。
*/
static final int MIN_TREEIFY_CAPACITY = 64;
/**
* 表,在第一次使用时被初始化,并根据需要调整大小。
* 必要时调整。当分配时,长度总是2的幂。
* (在某些操作中,我们也容忍长度为零,以允许
* 目前不需要的引导机制)。
*/
transient Node<K,V>[] table;
/**
* 保存缓存的 entrySet()。请注意,AbstractMap字段被用于
*用于keySet()和values()。
*/
transient Set<Map.Entry<K,V>> entrySet;
/**
*该地图中包含的键值映射的数量。
*/
transient int size;
/**
* 这个HashMap在结构上被修改的次数
* 结构性修改是指改变HashMap中的映射数量或以其他方式修改其内部结构的修改。
* HashMap或以其他方式修改其内部结构(例如。
* 重新洗牌)。 这个字段被用来使HashMap的集合视图上的迭代器失效。
* HashMap的迭代器快速失败。 (参见ConcurrentModificationException)。
*/
transient int modCount;
/**
* 下一个要调整的尺寸值(容量*负载系数)。扩容的阈值
*/
int threshold;
/**
* 加载因子
*/
final float loadFactor;
4.构造函数解析
4.1 无参构造
/**
* 无参构造
*/
public HashMap() {
//DEFAULT_LOAD_FACTOR=0.75
//这里只是做了给加载因子赋值0.75
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
4.2 有参构造
/**
* initialCapacity:初始化容量
* loadFactor:加载因子
*/
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;
//扩容的阈值
this.threshold = tableSizeFor(initialCapacity);
}
tableSizeFor
/**
* 返回给定目标容量的2次方大小。
* 这一步其实就是找比传入容量大的最近的一个2的幂次方的值
*/
static final int tableSizeFor(int cap) {
int n = cap - 1;
n |= n >>> 1;
n |= n >>> 2;
n |= n >>> 4;
n |= n >>> 8;
n |= n >>> 16;
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}
demo
int threshold = tableSizeFor(25);
System.out.println("threshold:" + threshold);
//分割线
int threshold = tableSizeFor(65);
System.out.println("threshold:" + threshold);
threshold:32
threshold:128
总结
在Hashmap1.8中,无参构造方法只是为加载因子赋默认值
有参构造也只是初始化一些常量值,并没有初始化表格的动作
留个疑问,table是在什么时候被初始化的?扩容又是在什么时候?
5.Node节点对象
static class Node<K,V> implements Map.Entry<K,V> {
//hash值
final int hash;
//key
final K key;
//calue
V value;
//下一个node节点
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() {
//异或运算,渐少hash碰撞
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;
}
}
6.HashMap.put()解析
public V put(K key, V value) {
//参数一:key的哈希值
//参数二:key
//参数三:value
//参数四:onlyIfAbsent if true, don't change existing value
//参数五:evict if false, the table is in creation mode.(创建模式)
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
* @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) {
//表格 节点 表格长度 bi奥格索引
Node<K,V>[] tab; Node<K,V> p; int n, i;
//如果在put时,表格还不存在,那么就开始初始化表格
if ((tab = table) == null || (n = tab.length) == 0)
//记录初始化表格的大小
n = (tab = resize()).length;
//如果表格存在就新建节点放在计算的tab[i]位置
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
//这里就是向链表或者红黑树中追加元素了
Node<K,V> e; K k;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
//key相同
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);
//判断是否达到转换为红黑树的条件
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
//key存在就更新值操作
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
//返回旧值
return oldValue;
}
}
//修改次数增加
++modCount;
//如果当前达到扩容的阈值就执行扩容操作
if (++size > threshold)
//扩容
resize();
afterNodeInsertion(evict);
return null;
}
resize()
/**
* 初始化或加倍表的大小。 如果为空,则按照
* 与字段阈值中持有的初始容量目标一致。
* 否则,因为我们使用的是2次方扩展,所以
* 每个bin的元素必须保持在相同的索引上,或者移动到
* 在新表中以2的幂数偏移。
*
* @返回表
*/
final Node<K,V>[] resize() {
//旧表格
Node<K,V>[] oldTab = table;
//旧表格的容量
int oldCap = (oldTab == null) ? 0 : oldTab.length;
//旧的临界值
int oldThr = threshold;
//初始化新表格的大小和临界值
int newCap, newThr = 0;
//如果旧数组的容量大于0,说明旧数组是存在的
if (oldCap > 0) {
//如果大于了最大容量值直接返回旧数组就行,没法扩容了
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
//这里是扩容操作,新数组是原来数组的两倍
//临界值也是原来临界值的两倍
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // double threshold
}
//如果旧数组的容量为0,而临界值存在,就将该临界值作为新数组的容量
else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;
else { // zero initial threshold signifies using defaults
//初始化新数组的容量为默认容量16
newCap = DEFAULT_INITIAL_CAPACITY;
//新数组的临界值为16*0.75=12
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;
//新建一个table数组,大小是newCap
@SuppressWarnings({"rawtypes","unchecked"})
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
//赋值给全局的table变量
table = newTab;
//如果是扩容操作,我们需要把旧数组的数据移动到新数组中去
if (oldTab != null) {
//遍历旧数组的每一个桶的位置,因为可能是链表或者红黑树的结构,所以需要循环遍历
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
if ((e = oldTab[j]) != null) {
//这一步就是释放旧数组J位置的空间
oldTab[j] = null;
//单节点的操作
if (e.next == null)
//重新计算哈希值,将e元素放在新数组中
newTab[e.hash & (newCap - 1)] = e;
else if (e instanceof TreeNode)
//红黑树的操作
((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;
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;
}
解析demo,首先假设哈希值4和哈希值12与上8运算
System.out.println("4 & 8="+(4 & 8));
System.out.println("12 & 8="+(12 & 8));
4 & 8=0
12 & 8=8
根据移动操作代码来操作我们可以画张图
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;
}
迁移前
迁移中
迁移后
总结
初始化表格是在第一次用到的时候,也就是put值的时候table才会被初始化,这样做有利于减少空间资源的浪费
扩容机制的触发是在put元素时发现当前table的中存储的元素达到了临界值,就开始扩容操作,初始化和扩容全部是在resize()方法中实现的
树化的阈值是8,而链表化的阈值是6,是因为两者之间如果频繁转换很影响性能,所以当红黑树移除一个元素时不会立即就触发转为链表的操作,提高性能和效率
hashmap1.8旧数组中的元素移到新数组时,低位的仍然保留在原来索引的位置,而高位的索引则是原来索引值+oldCap
7.HashMap.get()解析
public V get(Object key) {
Node<K,V> e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
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) {
//从链表的首节点开始查询,如果key相同就返回
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);
}
}
//如果找不到返回null
return null;
}
==与equals的区别
==是判断两个变量或实例是不是指向同一个内存空间 equals是判断两个变量或实例所指向的内存 空间的值是不是相同
==是指对内存地址进行比较 equals()是对字符串的内容进行比较
8.HashMap1.8为什么线程不安全
在put操作中有这么一行代码
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e; K k;
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);
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
注意这段代码
if ((p = tab[i = (n - 1) & hash]) == null){
//如果没有hash碰撞则直接插入元素
tab[i] = newNode(hash, key, value, null);
}
这是jdk1.8中HashMap中put操作的主函数, 注意如果没有hash碰撞则会直接插入元素。如果线程A和线程B同时进行put操作,刚好这两条不同的数据hash值一样,并且该位置数据为null,所以这线程A、B都会进入第6行代码中。
假设一种情况,线程A进入后还未进行数据插入时挂起,而线程B正常执行,从而正常插入数据,然后线程A获取CPU时间片,此时线程A不用再进行hash判断了,问题出现:线程A会把线程B插入的数据给覆盖,发生线程不安全。