一、HashMap成员变量
//默认初始table大小,16,2的次幂
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
//HashMap最大长度2的30次幂
static final int MAXIMUM_CAPACITY = 1 << 30;
//默认加载因子
static final float DEFAULT_LOAD_FACTOR = 0.75f;
//链表最大长度,大于8会转红黑树,
static final int TREEIFY_THRESHOLD = 8;
//红黑树最小节点数,小于6转链表
static final int UNTREEIFY_THRESHOLD = 6;
//转红黑树的最小table容量,table容量小于64,链表长度大于8,会执行扩容,并不会转红黑树
static final int MIN_TREEIFY_CAPACITY = 64;
//存放Node的数组
transient Node<K,V>[] table;
//将数据转换成set存储,用于迭代
transient Set<Map.Entry<K,V>> entrySet;
//实际存储的数量,则HashMap的size()方法,实际返回的就是这个值,isEmpty()也是判断该值是否为0
transient int size;
//hashmap结构被改变的次数,fail-fast机制(遍历时候修改数据,抛ConcurrentModificationException)
transient int modCount;
//HashMap的扩容阈值,在HashMap中存储的Node键值对超过这个数量时,自动扩容容量为原来的二倍
int threshold;
//HashMap的负加载因子,可计算出当前table长度下的扩容阈值:threshold = loadFactor * table.length
final float loadFactor;
transient 关键字:transient关键字只能修饰变量,而不能修饰方法和类,被其修饰的变量不会被序列化。换言之,将不需要序列化的变量前添加关键字transient,序列化对象的时候,这个属性就不会被序列化。值得注意的是静态变量不管是否被transient修饰,均不能被序列化。
二、HashMap流程图
三、HashMap的构造方法
//使用指定的初始化容量initialcapacity 和加载因子loadfactor构造一个空HashMap
public HashMap(int initialCapacity, float loadFactor) {
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " + initialCapacity);
//初始化容量不得大于最大容量2的30次幂
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " + loadFactor);
//使用指定加载因子
this.loadFactor = loadFactor;
//处理指定的初始化容量(不管输入多少,取该数最接近的2的次幂,下面有该方法解释)
this.threshold = tableSizeFor(initialCapacity);
}
//使用指定的初始化容量initial capacity和默认加载因子DEFAULT_LOAD_FACTOR(0.75)构造一个空HashMap
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
//使用指定的初始化容量(16)和默认加载因子DEFAULT_LOAD_FACTOR(0.75)构造一个空HashMap
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
// 使用指定Map m构造新的HashMap。默认加载因子DEFAULT_LOAD_FACTOR(0.75,初始化容量随m.size变化
public HashMap(Map<? extends K, ? extends V> m) {
this.loadFactor = DEFAULT_LOAD_FACTOR;
putMapEntries(m, false);
}
四、HashMap的put(),putVal()方法
//如果value被替换,则返回旧的value,否则返回null。
//倒数第二个参数false:表示允许旧值替换
//最后一个参数true:表示HashMap不处于创建模式
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
/**
* @param hash 指定参数key的哈希值
* *@param key 指定参数key
* @param value 指定参数value
* @param onlyIfAbsent 如果为true,即使指定参数key在map中已经存在,也不会替换value
* @param evict 如果为false,数组table在创建模式中
* @return 如果value被替换,则返回旧的value,否则返回null。当然,可能key对应的value就是null。
*/
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
//此处初始化hashmap
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;
//当前值与该下标中第一个值匹配,则记录当前下标中的值e(后面会执行新旧value的替换)
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
//为树结构,走红黑树查询返回匹配的TreeNode赋值给e,TreeNode继承Node,如果红黑树中未匹配到key则新增树节点,返回null赋值给e
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
//链表中匹配到Key则返回Node赋值给e,未匹配到执行新增返回null赋值给e
else {
for (int binCount = 0; ; ++binCount) {
// 遍历到链表尾部,执行新增,此时e为null。注意该if条件执行完会将p.next赋值给e
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
//检查链表长度是否达到阈值8,达到则转为红黑树
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,因为上面代码已经将匹配成功节点赋值给了e。
break;
// p = e将当前节点赋值给p,与上面e = p.next联合,构成了一个循环,建议多读几遍代码
p = e;
}
}
//如果存在原数据key与新数据匹配成功,e则不为null
if (e != null) { // existing mapping for key
V oldValue = e.value;
//匹配成功没有直接做value替换,只有onlyIfAbsent 为false,才替换旧值
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
//只有存在匹配成功的key才返回旧值
return oldValue;
}
}
//hashMap的结构改变次数加1,遍历会用到改变量
++modCount;
//键值对超过阀值,进行扩容
if (++size > threshold)
resize();
//该方法可以忽略,是为继承HashMap的LinkedHashMap类服务的。保证LinkedHashMap的有序性,可能以后会写一篇LinkedHashMap的源码讲解
afterNodeInsertion(evict);
//新节点做的插入,返回null
return null;
}
有不懂,评论区留言,下班写的,时间比较仓促!
五、HashMap的hash()与tab[i = (n - 1) & hash]的取下标运算
5.1源码讲解
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
1.上面为hash()方法,将key=null的hash值直接置为了0,putVal方法没有对key=null的数据专门处理,这点与jdk7不同,因为key=null的数据在计算hash时已经做了处理,后面计算下标“与”运算时key=null的下标始终为0。
2.非null的key计算hash时,做了(h=hashCode() )^ (h >>> 16)运算,将hashCode低16位与其高16位(int:32位)做了异或(相同为0,不同为1)运算,加大低位的随机性,加大了hash的随机性,减少了碰撞。
5.2为何哈希 table 的大小控制为 2 的次幂数?
tab[i = (n - 1) & hash],其中n是table的长度也就是Node[]的长度,(n - 1) & hash为下标,为了使下标尽量的分散均匀,需要hash值对table长度做取模运算也就是hash % n,但是代码并不是这么写的。
原因1.当哈希 table 的大小控制为 2 的次幂数时满足,(n - 1) & hash = hash % n,两者等价不等效,“与”运算效率更高。取模运算降低发生碰撞的概率,使散列更均匀,实验得知,可以自己动手试试。
原因2.table 长度为 2 的次幂,那么(length-1)的二进制最后一位一定是 1,在对 hash 值做“与”运算时,最后一位就可能为 1,也可能为 0,换句话说,取模的结果既有偶数,又有奇数。设想若(length-1)为偶数,那么“与”运算后的值只能是 0,奇数下标的 bucket 就永远散列不到,会浪费一半的空间。
原因3.扩容在重新计算链表所处于桶的下标时,用了一个计算:(e.hash & oldCap) == 0表示该节点还在原下标,此计算要求oldCap二进制格式低位为0,也就是oldCap需要为2次幂。
六、HashMap的resize()方法
/**
* 对table进行初始化或者扩容。
* 如果table为null,则对table进行初始化
* 如果对table扩容,因为每次扩容都是翻倍,与原来计算(n-1)&hash的结果相比,节点要么就在原来的位置,要么就被分配到“原位置+旧容量”这个位置
* resize的步骤总结为:
* 1.计算扩容后的容量,临界值。
* 2.将hashMap的临界值修改为扩容后的临界值
* 3.根据扩容后的容量新建数组,然后将hashMap的table的引用指向新数组。
* 4.将旧数组的元素复制到table中。
*
* @return the table
*/
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;
int newCap, newThr = 0;
//第一个if条件处理的是已经初始化过的table
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
}
//第二个if处理的是未经过初始化的table,给定初始容量的table
else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;
//第三个if处理的是未经过初始化的table,且未给定初始容量
else { // zero initial threshold signifies using defaults
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
//只有上面第二个if条件满足
if (newThr == 0) {
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
threshold = newThr;
//@SuppressWarnings该注解用于消除警告
@SuppressWarnings({"rawtypes","unchecked"})
//创建新table,赋值新的初始化容量
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;
//如果旧table不为空,将旧table中的元素复制到新的table中
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)
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
//桶中数据结构是链表 讲解一下 这个代码挺有意思
else { // preserve order
//loHead 高位链表头结点 loTail 高位链表尾结点
Node<K,V> loHead = null, loTail = null;
//hiHead 低位链表头结点 hiTail 低位链表尾结点
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
do {
//下面代码 配合e = next实现循环
next = e.next;
//oldCap与newCap-1最高位一致都是1(无补位情况下),而newCap-1比oldCap-1多一个高位1,可能会影响e.hash & length-1值,如果e.hash在newCap-1最高位位置为0则不影响值,否则影响,而oldCap是2次幂二进制表示最高位1其他位0(无补位情况下),所以与e.hash & oldCap是否为0等效
if ((e.hash & oldCap) == 0) {
//该情况下容量扩容后不影响下标,计算试一下就知道e.hash & oldCap-1 = e.hash & newCap-1
if (loTail == null)
//e的引用给loHead
loHead = e;
else
//修改loTail引用所指向的对象,也就是修改loHead引用所指向的对象
loTail.next = e;
//e的引用给loTail
loTail = e;
}
else {
//该情况下容量扩容后影响下标,计算试一下就知道e.hash & newCap-1 = (e.hash & oldCap-1)+oldCap 扩容后newCap-1多的最高位1也就是oldCap
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
if (loTail != null) {
//尾节点指向null
loTail.next = null;
//低位链表还在原位置
newTab[j] = loHead;
}
if (hiTail != null) {
hiTail.next = null;
//高位链表下标位置发生变化
newTab[j + oldCap] = hiHead;
}
}
}
}
}
//返回扩容后Node数组
return newTab;
}
七、HashMap的tableSizeFor()方法,保证容量永远为2次幂
扩容时候是位移运算保证了容量为2次幂,
但是初始化hashMap指定容量时怎么保证的呢?
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;
//tableSizeFor处理了用户的初始化容量
this.threshold = tableSizeFor(initialCapacity);
}
以上代码可见,走了tableSizeFor()方法,看一下源码:
//将cap转为>=cap的最小2的自然数幂,返回
static final int tableSizeFor(int cap) {
//此处减1是因为当cap本身为2次幂时,最终返回的是cap不是cap*2
int n = cap - 1;
//能确定n的最高位为1(除去补位),n>>>1将高位1左移1位,n与n>>>1做或运算,则高位,次高位为1
n |= n >>> 1;
n |= n >>> 2;
n |= n >>> 4;
n |= n >>> 8;
//处理到此处则将n的所有位数都变为1(除去补位),最后一位为1,也就导致此处一定是奇数
n |= n >>> 16;
//n+1处理将奇数调整为2次幂
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}
七、HashMap的get(),getNode()方
/**
* 返回指定的key映射的value,如果value为null,则返回null
* get可以分为三个步骤:
* 1.通过hash(Object key)方法计算key的哈希值hash。
* 2.通过getNode( int hash, Object key)方法获取node。
* 3.如果node为null,返回null,否则返回node.value。
*/
public V get(Object key) {
Node<K,V> e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
/**
* 根据key的哈希值和key获取对应的节点
* getNode可分为以下几个步骤:
* 1.如果哈希表为空,或key对应的桶为空,返回null
* 2.如果桶中的第一个节点就和指定参数hash和key匹配上了,返回这个节点。
* 3.如果桶中的第一个节点没有匹配上,而且有后续节点
* 3.1如果当前的桶采用红黑树,则调用红黑树的get方法去获取节点
* 3.2如果当前的桶不采用红黑树,即桶中节点结构为链式结构,遍历链表,直到key匹配
* 4.找到节点返回,否则返回null。
*/
final Node<K,V> getNode(int hash, Object key) {
Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
//table不为空执行逻辑,否则返回null
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的remove(),removeNode(),clear()方法
/**
* 删除hashMap中key映射的node
* remove方法的实现可以分为三个步骤:
* 1.通过 hash(Object key)方法计算key的哈希值。
* 2.通过 removeNode 方法实现功能。
* 3.返回被删除的node的value。
* @param key 参数key
* @return 如果没有映射到node,返回null,否则返回对应的value
*/
public V remove(Object key) {
Node<K,V> e;
return (e = removeNode(hash(key), key, null, false, true)) == null ?
null : e.value;
}
/**
* removeNode方法的步骤总结为:
* 1.如果数组table为空或key映射到的桶为空,返回null。
* 2.如果key映射到的桶上第一个node的就是要删除的node,记录下来。
* 3.如果桶内不止一个node,且桶内的结构为红黑树,记录key映射到的node。
* 4.桶内的结构不为红黑树,那么桶内的结构就肯定为链表,遍历链表,找到key映射到的node,记录下来。
* 5.如果被记录下来的node不为null,删除node,size-1
* 6.返回被删除的node。
*
* @param hash key的哈希值
* @param key key的哈希值
* @param value 如果 matchValue 为true,则value也作为确定被删除的node的条件之一,否则忽略
* @param matchValue 如果为true,则value也作为确定被删除的node的条件之一
* @param movable 如果为false,删除node时不会删除其他node
* @return 返回被删除的node,如果没有node被删除,则返回null(针对红黑树的删除方法)
*/
final Node<K,V> removeNode(int hash, Object key, Object value,
boolean matchValue, boolean movable) {
Node<K,V>[] tab; Node<K,V> p; int n, index;
if ((tab = table) != null && (n = tab.length) > 0 &&
(p = tab[index = (n - 1) & hash]) != null) {
Node<K,V> node = null, e; K k; V v;
//桶中第一个元素是要删除的元素
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
node = p;
//桶中第一个元素不是要删除的元素,且桶中存在其他元素
else if ((e = p.next) != null) {
//桶中元素是红黑树
if (p instanceof TreeNode)
//红黑树中查询该元素
node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
else {
//桶中元素是链表结构
do {
if (e.hash == hash &&
((k = e.key) == key ||
(key != null && key.equals(k)))) {
//找到该元素
node = e;
break;
}
p = e;
} while ((e = e.next) != null);
}
}
//如果得到的node不为null且(matchValue为false||node.value和参数value匹配)
if (node != null && (!matchValue || (v = node.value) == value ||
(value != null && value.equals(v)))) {
if (node instanceof TreeNode)
//使用红黑树的删除方法删除node
((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
else if (node == p)
//桶中第一个元素就是要删除的node
tab[index] = node.next;
else
//链表方式删除node
p.next = node.next;
//结构性修改次数加一
++modCount;
//哈希表大小减一
--size;
afterNodeRemoval(node);
//返回删除的node
return node;
}
}
return null;
}
/**
* 删除map中所有的键值对
**/
public void clear() {
Node<K,V>[] tab;
//结构性修改次数加一
modCount++;
if ((tab = table) != null && size > 0) {
//哈希表大小置为0
size = 0;
//每个桶中的数据置为null
for (int i = 0; i < tab.length; ++i)
tab[i] = null;
}
}
八、HashMap的7种遍历方式以及Iterator类解析
8.1 7种遍历方式
Map<String,Object> map= new HashMap<String, Object>();
map.put("1","测试1");
map.put("2","测试2");
map.put("3","测试3");
//遍历方式1 Iterator 迭代器 迭代entrySet(推荐此方法,数据量大时速度快)
Iterator<Map.Entry<String,Object>> iterator = map.entrySet().iterator();
while (iterator.hasNext()){
Map.Entry<String,Object> entry = iterator.next();
System.out.printf("key= " + entry.getKey() + " and value= " + entry.getValue());
//遍历方式2 Iterator 迭代器 迭代keySet(只需要操作key时推荐使用,不建议再次走map.get()方法)
Iterator<String> keyIterator = map.keySet().iterator();
while (keyIterator.hasNext()){
String key = keyIterator.next();
System.out.printf("key= " + key + " and value= " + map.get(key));
}
//遍历方式3 Iterator 迭代器 迭代values(只操作value使用)
Iterator<Object> valueIterator = map.values().iterator();
while(valueIterator.hasNext()){
Object value = valueIterator.next();
System.out.printf("value= " + value);
}
//遍历方式4 遍历entrySet(推荐此方法,数据量大时速度快)
for(Map.Entry<String,Object> entry : map.entrySet()){
System.out.printf("key= " + entry.getKey() + " and value= " + entry.getValue());
}
//遍历方式5 遍历keySet(只需要操作key时推荐使用,不建议再次走map.get()方法)
for(String key : map.keySet()){
System.out.printf("key= " + key + " and value= " + map.get(key));
}
//遍历方式6 遍历values(只操作value使用)
for (Object value : map.values()) {
System.out.println("value= " + value);
}
//遍历方式7 foreach方法
map.forEach((key,value) -> {
System.out.printf("key= " + key + " and value= " + value);
});
}
上述7种遍历方式其实3种使用迭代器形式,4种foreach,分别是对entry对象遍历,key遍历,value遍历,其实for与foreach是一种语法糖,实际执行的还是iterator。
说到遍历就不得不提,关于在遍历时删除数据抛异常的问题。
8.2 Iteretor解析
看一下关于Iterator的继承关系图:
Iterator接口代码:
boolean hasNext();
E next();
default void remove() {
throw new UnsupportedOperationException("remove");
}
default void forEachRemaining(Consumer<? super E> action) {
Objects.requireNonNull(action);
while (hasNext())
action.accept(next());
}
Iterator接口非常简单就4个接口方法,HashMap中新增了一个内部抽象类HashIterator实现了这4个方法,看一下代码:
abstract class HashIterator {
Node<K,V> next; // next entry to return
Node<K,V> current; // current entry
//hashmap结构改动次数
int expectedModCount; // for fast-fail
int index; // current slot
HashIterator() {
expectedModCount = modCount;
Node<K,V>[] t = table;
current = next = null;
index = 0;
if (t != null && size > 0) { // advance to first entry
do {} while (index < t.length && (next = t[index++]) == null);
}
}
public final boolean hasNext() {
return next != null;
}
final Node<K,V> nextNode() {
Node<K,V>[] t;
Node<K,V> e = next;
//判断了一下当前modCount 与expectedModCount是否相等,如果发生了结构化改动执行了hashMap的put,或者remove,modCount将大于expectedModCount
//修改次数的一致性校验,防止遍历时有并发的一致性校验。
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
if (e == null)
throw new NoSuchElementException();
if ((next = (current = e).next) == null && (t = table) != null) {
do {} while (index < t.length && (next = t[index++]) == null);
}
return e;
}
//这个方法是我们是要关注的 实现了Iterator接口中的删除方法,与HashMapremove()方法不同
public final void remove() {
Node<K,V> p = current;
if (p == null)
throw new IllegalStateException();
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
current = null;
K key = p.key;
//调用HashMap的removeNode方法
removeNode(hash(key), key, null, false, false);
//该处是重点,在原来的removeNode方法之后,又重新赋值给expectedModCount 所以如果执行的删除是HashIterator的删除将不会在nextNode抛异常,执行的是HashMap的删除会抛异常。
expectedModCount = modCount;
}
}
现在关于在遍历hashMap时,删除节点为何会抛异常相信都很明了了。因为使用的remove方法不一致,我们在for遍历的时候,使用的删除节点的方法是hashMap提供的remove()方法,该方法没有在删除后更新Iteretor中expectedModCount 变量,导致执行nextNode()方法时抛异常ConcurrentModificationException。Iteretor遍历时删除写法为 iterator.remove(),则不存在该问题。
下面写几个代码示例:
public static void main(String[] args) {
Map<String,Object> map= new HashMap<String, Object>();
map.put("1","测试1");
map.put("2","测试2");
map.put("3","测试3");
for(String key : map.keySet()){
System.out.printf("key= " + key + " and value= " + map.get(key));
//该方法会导致抛异常ConcurrentModificationException
map.remove(key);
}
Iterator<String> keyIterator = map.keySet().iterator();
while (keyIterator.hasNext()){
String key = keyIterator.next();
System.out.printf("key= " + key + " and value= " + map.get(key));
if(key.equals("1")){
//iteretor提供的remove方法不会导致异常
keyIterator.remove();
}
}
Iterator<Map.Entry<String,Object>> iterator = map.entrySet().iterator();
while (iterator.hasNext()){
Map.Entry<String,Object> entry = iterator.next();
System.out.printf("key= " + entry.getKey() + " and value= " + entry.getValue());
if(entry.getKey().equals("1")){
//iteretor提供的remove方法不会导致异常
iterator.remove();
}
}
}
另外在遍历时,如果发生节点的新增,也会导致modCount++,抛异常。同一key的value值替换则不会导致modCount++,不抛异常。
8.3 fail-fast 策略
fail-fast就是在系统设计中,当遇到可能会诱导失败的条件时立即上报错误,快速失效系统往往被设计在立即终止正常操作过程,而不是尝试去继续一个可能会存在错误的过程。
再简洁点说,就是尽可能早的发现问题,立即终止当前执行过程,由更高层级的系统来做处理。
在 HashMap 中,我们前面提到的modCount域变量,就是用于实现 hashMap 中的fail-fast。出现这种情况,往往是在非同步的多线程并发操作。
在对 Map 的做迭代(Iterator)操作时,会将modCount域变量赋值给expectedModCount局部变量。在迭代过程中,用于做内容修改次数的一致性校验。若此时有其他线程或本线程的其他操作对此 Map 做了内容修改时,那么就会导致modCount和expectedModCount不一致,立即抛出异常ConcurrentModificationException。
但也要注意一点,能安全删除,并不代表就是多线程安全的,在多线程并发执行时,若都执行上面的操作,因未设置为同步方法,也可能导致modCount与expectedModCount不一致,从而抛异常ConcurrentModificationException。
九、HashMap、HashTable 关系
9.1 共同点和异同点
共同点:
底层都是使用哈希表 + 链表的实现方式。
区别:
1.从层级结构上看,HashMap、HashTable 有一个共用的Map接口。另外,HashTable 还单独继承了一个抽象类Dictionary;
2.HashTable 诞生自 JDK1.0,HashMap 从 JDK1.2 之后才有;
3.HashTable 线程安全,HashMap 线程不安全;
4.初始值和扩容方式不同。HashTable 的初始值为 11,扩容为原大小的2*d+1。容量大小都采用奇数且为素数,且采用取模法,这种方式散列更均匀。但有个缺点就是对素数取模的性能较低(涉及到除法运算),而 HashTable 的长度都是 2 的次幂,设计就较为巧妙,前面章节也提到过,这种方式的取模都是直接做位运算,性能较好。
5.HashMap 的 key、value 都可为 null,且 value 可多次为 null,key 多次为 null 时会覆盖。当 HashTable 的 key、value 都不可为 null,否则直接 NPE(NullPointException)。
9.2 HashMap 的线程安全
1.,比如添加synchronized关键字,或者使用lock机制。而 HashTable 使用了前者,即synchronized关键字。
2.put 操作、get 操作、remove 操作、equals 操作,都使用了synchronized关键字修饰。
这么做是保证了 HashTable 对象的线程安全特性,但同样也带来了问题,突出问题就是效率低下。为何会说它效率低下呢?
1.因为按 synchronized 的特性,对于多线程共享的临界资源,同一时刻只能有一个线程在占用,其他线程必须原地等待,为方便理解,
大家不妨想下计时用的沙漏,中间最细的瓶颈处阻挡了上方细沙的下落,同样的道理,当有大量线程要执行get()操作时,也存在此类问题,大量线程必须排队一个个处理。
2.这时可能会有人说,既然get()方法只是获取数据,并没有修改 Map 的结构和数据,不加不就行了吗?不好意思,不加也不行,别的方法都加,就你不加,会有一种场景,
那就是 A 线程在做 put 或 remove 操作时,B 线程、C 线程此时都可以同时执行 get 操作,可能哈希 table 已经被 A 线程改变了,也会带来问题,因此不加也不行。
3.现在好了,HashMap 线程不安全,HashTable 虽然线程安全,但性能差。解决办法是使用ConcurrentHashMap类,线程安全,操作高效。
十、HashMap 的线程不安全问题
10.1数据覆盖问题
a、b两个线程同时执行put()操作,且两个线程的key对应一个桶,都是执行的新节点的插入,如果a线程获取到了该桶中的最后一个节点,此时线程时间片用完b线程开始执行,b线程将新节点插入到了旧尾节点的后面。当a线程再次执行的时候,依旧将节点插入到旧节点的尾部,则会覆盖掉b节点的数据,导致数据丢失。
并不只有put()方法会导致数据的覆盖,其他方法像删除、修改同样会存在问题。
10.2扩容时死循环问题
只有 JDK7 及以前的版本会存在死循环现象,在 JDK8 中,resize()方式已经做了调整,使用两队链表,且都是使用的尾插法,及时多线程下,也顶多是从头结点再做一次尾插法,不会造成死循环。而 JDK7 能造成死循环,就是因为 resize()时使用了头插法,将原本的顺序做了反转,才留下了死循环的机会。
JDK7 中的扩容代码片段:
void transfer(Entry[] newTable, boolean rehash) {
//新table初始化大小
int newCapacity = newTable.length;
//for循环原table的每一个节点
for (Entry<K,V> e : table) {
//当前节点为null停止循环
while(null != e) {
//将当前节点e的下一节点赋给next
Entry<K,V> next = e.next;
//重新计算hash
if (rehash) {
e.hash = null == e.key ? 0 : hash(e.key);
}
//计算新的下标i
int i = indexFor(e.hash, newCapacity);
//将e的下一节点指向原先的桶中数据,此时e节点构成一个链表,第一个值是当前while中遍历的节点,每次循环都如此,也就是将原链表进行了倒转
e.next = newTable[i];
//e放入桶中
newTable[i] = e;
//将e指向下一节点用于循环
e = next;
}
}
}
十一、HashMap 的线程不安全问题解决方案
11.1 使用Collections.SynchronizedMap()方法,示例代码:
Map<String, Integer> testMap = new HashMap<>();
...
// 转为线程安全的 map
Map<String, Integer> map = Collections.synchronizedMap(testMap);
其内部实现也很简单,等同于 HashTable,只是对当前传入的 map 对象,新增对象锁(synchronized):
private static class SynchronizedMap<K,V> implements Map<K,V>, Serializable {
private static final long serialVersionUID = 1978198479659022715L;
private final Map<K,V> m; // Backing Map
final Object mutex; // Object on which to synchronize
SynchronizedMap(Map<K,V> m) {
this.m = Objects.requireNonNull(m);
mutex = this;
}
SynchronizedMap(Map<K,V> m, Object mutex) {
this.m = m;
this.mutex = mutex;
}
public int size() {
synchronized (mutex) {return m.size();}
}
public boolean isEmpty() {
synchronized (mutex) {return m.isEmpty();}
}
public boolean containsKey(Object key) {
synchronized (mutex) {return m.containsKey(key);}
}
public boolean containsValue(Object value) {
synchronized (mutex) {return m.containsValue(value);}
}
public V get(Object key) {
synchronized (mutex) {return m.get(key);}
}
public V put(K key, V value) {
synchronized (mutex) {return m.put(key, value);}
}
public V remove(Object key) {
synchronized (mutex) {return m.remove(key);}
}
public void putAll(Map<? extends K, ? extends V> map) {
synchronized (mutex) {m.putAll(map);}
}
public void clear() {
synchronized (mutex) {m.clear();}
}
}
11.2 使用 ConcurrentHashMap
同 HashMap 的使用。
JDK1.5 版本引入,位于并发包java.util.concurrent下。
在 JDK7 版本及以前,ConcurrentHashMap类使用了分段锁的技术(segment + Lock),但在 jdk8 中,也做了较大改动,使用回了 synchronized 修饰符。
Map<String,Object> cuMap= new ConcurrentHashMap<String,Object>();
cuMap.put("1","测试1");
cuMap.put("2","测试2");
cuMap.put("3","测试3");