面试官:了解集合吗?
你:了解一点点
面试官:那你讲一下hashmap吧
你:hashmap就是数组+链表的一种结构,使用键值对,键不能重复,值可以重复
面试官:还有呢?
你:。。。
面试官:讲一下resize的过程?
你:。。。
面试官:讲一下hashmap是不是线程安全的?
你:hashmap是线程不安全的
面试官:为什么是线程不安全的?
你:。。。
面试官:好的,这次面试就到这里吧,我们下次联系(内心os:这小逼崽子什么都不会,联系你才怪了)
文章目录
1、什么是HashMap?
我们知道,Java集合中最重要的三种集合:
Set 不允许重复
List 允许重复,有序
Map 键值对
我们不仅要了解三种的特性,还要了解他们具体的一些实现类
Set
HashSet
TreeSet
List
ArrayList
LinkedList
Map
HashMap
ConcurrentHashMap
HashTable
SynchronizedMap
那么什么是HashMap呢?
public class HashMap<K,V> extends AbstractMap<K,V>
implements Map<K,V>, Cloneable, Serializable {
//……
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
}
从类定义中,我们可以看到,首先它是继承自AbstractMap,实现了Map,Cloneable,Serializable
所以,他是一种键值对的数据结构,继承自AbstractMap,实现了Map接口
2、讲一下HashMap 的底层原理
因为HashMap有两个版本,分为jdk1.7以前的和jdk1.8以后的。我们这里分开来讲
------------------------jdk1.7-------------------------
jdk1.7以前,HashMap底层使用的是数据+链表的数据结构,并且使用的是头插法
在初始化的时候呢,默认的初始化容量是16,默认加载因子是0.75
什么是加载因子?
加载因子就是当容量使用情况到达:16(当前容量)*0.75(加载因子)的时候,就会
进行一次resize()操作,也就是扩容
什么是resize()扩容?具体是怎么操作的?
举个栗子:当前我的容量为16,默认的加载因子为0.75,当我HashMap里的容量
使用为12的时候,我还想往HashMap里面去添加数据,此时HashMap就会触发扩容。
resize()的过程:
1、首先是创建一个新的数组,长度为16(当前容量)*2=32
2、然后使用头插法,将HashMap里面的元素进行rehash计算,再把元素映射
到新的数组中去
为什么默认的初始化容量是16?
因为16的二进制为1111,而进行hash计算的时候,我们是获得key的hashCode()
再和数组长度进行与操作,来进行hash运算的。所以当为2次幂的时候,能够实现最大均
匀,至于为什么是16不是8或者32的话,可能是为了均匀分布吧
index = HashCode(Key) & (Length- 1)
------------------------jdk1.8-------------------------
jdk1.8基本上和jdk1.7一样,做的改变就是:
1、底层数据结构
jdk1.7采用的是数组+链表
jdk1.8采用的是数组+链表+红黑树
2、resize()
jdk1.7,采用头插法
jdk1.8,采用尾插法
什么时候从链表转为红黑树?
因为红黑树的平均查找长度是log(n),链表的平均查找长度是n/2,长度为8的时候,
平均查找长度为3,如果继续使用链表,平均查找长度为8/2=4,这才有转换为树的必要。
链表长度如果是小于等于6,6/2=3,虽然速度也很快的,但是转化为树结构和生成树的时
间并不会太短。
3、HashMap里面有什么参数?
1、存放数据的结构数组
2、结构体Node
3、默认初始化长度default——capacity
4、最大的长度2^30
5、默认加载因子0.75
6、jdk1.8之后,加入了链表与红黑树的转化,
于是又一个转换为红黑树的阈值8和转换为链表的阈值6
7、modcount
4、HashMap怎么处理hash冲突的?
上面讲了Hash Map是一个数组加链表来实现的,(jdk1.8之后加入了红黑树优化),
我们首先会对key的值进行hash运算,得到一个hash值存储到数组中,当然了,可能又很
多的key值映射到同一个位置上,所以,所有映射到同一位置上的值,会以链表的映射存储
,jdk1.7使用的是头插法,jdk1.8之后使用的为尾插法
5、HashMap存取原理?
put
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
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
* @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; 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;
}
get
/**
* Returns the value to which the specified key is mapped,
* or {@code null} if this map contains no mapping for the key.
*
* <p>More formally, if this map contains a mapping from a key
* {@code k} to a value {@code v} such that {@code (key==null ? k==null :
* key.equals(k))}, then this method returns {@code v}; otherwise
* it returns {@code null}. (There can be at most one such mapping.)
*
* <p>A return value of {@code null} does not <i>necessarily</i>
* indicate that the map contains no mapping for the key; it's also
* possible that the map explicitly maps the key to {@code null}.
* The {@link #containsKey containsKey} operation may be used to
* distinguish these two cases.
*
* @see #put(Object, Object)
*/
public V get(Object key) {
Node<K,V> e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
/**
* Implements Map.get and related methods.
*
* @param hash hash for key
* @param key the key
* @return the node, or null if none
*/
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;
}
6、HashMap为什么是线程不安全的?
还记得我们之前讲的,jdk1.7以前,使用的是头插法;jdk1.8以后使用的是尾插法
那么线程不安全就发生在这个resize()的过程
假设我们现在有一个HashMap,当前容量为10,当HashMap里面已经有7个元素的时候,
我想在插入一个,肯定就触发扩容机制resize(),扩容了
假设HashMap里面有一个有这么两个元素("lignque",1)和("queling",1),
假设我们恰巧进行hash映射的时候,映射到同一个位置,
使用链表存储的指向为("lignque",1)->("queling",2)
此时有两个线程去操作这个HashMap,线程1和线程2
(此时里面已经有7个元素了,再插入一个就会发生扩容resize())
都想往里面插入一个元素
--------------------------------------------------
线程1 | 线程2
--------------------------------------------------
插入一个元素,发现需要扩容 |
--------------------------------------------------
| 插入一个元素,发现需要扩容
--------------------------------------------------
把数组扩容到原来的两倍(20) |
--------------------------------------------------
将原来的元素重新计算hash值 |
映射到新的数组里面,变成了 |
("queling",2)->("lingque",1)|
--------------------------------------------------
| 把数组扩容到原来的两倍(20)
--------------------------------------------------
| rehash,复制过去,首先定位到
| ("lignque",1),他的下一个为
| ("queling",2),但是线程1已经进行了
| resize()操作,此时("queling",2)->("lingque",1)
| 此时两个元素就相互为next,形成死循环
--------------------------------------------------
7、有什么安全的替代?
- HashTable
- ConCurrentHashMap
- SynchronizedMap
敖丙:我把ConcurrentHashMap & HashTable的知识点都整理了一下,讲的很详细,推荐一波