Map 相关
1. HashMap 是线程安全的吗,为什么不是线程安全的(最好画图说明)?
不是线程安全的
// HashMap 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) // TODO 1 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; }
如果有两个线程A,B,都进行了插入数据,刚好这两条不同数据经过hash计算后得到的哈希码是一样的,且该位置还没有其他数据。两个线程都到达 // TODO 1 位置,现在A执行判断为null,进入if代码块(还未执行数据赋值),然后CPU将资源让给B执行,B判断为null,进入if代码块。现A,B都在if代码块内,所以后操作的会覆盖先操作的,发生线程不安全情况。
如图:当出现多个线程对同一个节点(位置)进行操作时,出现不安全情况

在扩容的时候也可能会导致数据不一致,因为扩容是从一个数组拷贝到另外一个数组
2. HashMap的扩容过程
当向容器添加元素的时候,会判断当前容器的元素个数,如果大于等于阈值——即当前数组的长度乘以加载因子的值的时候,就要自定扩容了(扩容条件)
扩容(resize)就是重新计算容量,向HashMap对象里不停的添加元素,而HashMap对象内部的数组无法装载更多元素的时,对象就需要扩大数组的长度,以便于能装入更多的元素。java里的数组是无法自动扩容的,方法使用的一个新数组代替已有的容量小的数组,就像我们用一个小桶装水,如果想要装更多的水,只能换大桶
// Jdk 1.8 扩容 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 (oldCap > 0) {
// 超过最大了 if (oldCap >= MAXIMUM_CAPACITY) { threshold = Integer.MAX_VALUE; return oldTab; }
// 计算新数组的长度 else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY)
// * 2 newThr = oldThr << 1; // double threshold } 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) ((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; }
3. HashMap 1.7与1.8的区别,说明1.8做了哪些优化,如何优化的?
HashMap 结构图:

在JDK1.7及之前的版本中,HashMap又叫做散列链表:基于一个数组以及多个链表的实现,hash值冲突的时候,就将对应节点以链表的形式存储(组织)
JDK1.8中,当同一个hash值(Table上元素)的链表节点数不小于8时,将不在以单链表的形式存储了,会被调整成一个红黑树,这就是JDK7和JDK8的实现的最大区别
JDK1.7:使用一个Entry数组来存储数据,用key的 hashcode 取模来决定key会被放到数组里的位置(index),如果hashcode相同,或者hashcode取模后的结果相同(hash collision),那么这些key会被定位到Entry数组的统一个格子里,这些key会行程一个链表。在 hashcode 特别差的情况下,比如说所有key的hashcode都相同,这个链表可能会很长,那么put/get操作都可能需要遍历这个链表,也就是说时间复杂度在最差的情况下会退化到 o(n)
JDK 1.8:使用一个Node 数组来存储数据,但这个Node 可能是链表,也可能是红黑树结构
-
- 如果插入的key的hashcode相同,那么这些key也会被定位到Node数组的同一个格子里
- 如果同一个格子里的key不超过8个,使用链表结构储存
- 如果超过了8个,那么会调用 treeifyBin 函数,将链表转换为红黑树
那么即使hashcode 完全相同,由于红黑树的特点,查找某个特定元素,也只需要O(log n) 的开销,也就是说put/get的操作时间复杂度最差只有O(log n),但是真正想要利用JDK1.8的好处,有一个限制:key的对象,必须正确的实现了Compare接口,如果没有实现Compare接口,或者实现得不正确,那JDK1.8的HashMap其实还是慢于 JDK1.7的
4. LinkedHashMap 的应用
基于LinkedHashMap 的访问熟顺序的特点,可构造一个 LRU(Least Recently Used) 最近最少使用简单缓存,也有一些开源的缓存产品如 ehcache 的淘汰策略(LRU)就是在LinkedHashMap 上扩展的
浙公网安备 33010602011771号