HashMap的原理
-
HashMap在JDK1.8后是基于数组+链表+红黑树来实现的,特点是键不重复,值可以重复,键可以为null,线程不安全
-
HashMap的扩容机制:默认的容量为16,默认的负载因子为0.75,当添加的数据达到超过容量乘负载因子的时候,就会创建一个新的容量为前一次两倍的新数组,再将原来的数组复制到新数组中,当数组的长度达到64并且链表长度大于8的时候,链表就会转换为红黑树
-
HashMap的存取原理:
- 计算key的hash值,然后进行二次hash ,根据二次hash结果找到对应的索引位置
- 如果这个位置有值,那么就用equals进行比较,如果为true那么久替换原来的元素,,如果为false那么就使用高低位平移法将结点插入到链表中(JDK8以前使用头插法,但是头插法在并发扩容时可能会造成环形链表或数据丢失,而高低位平移发会发生数据覆盖的情况))
JDK1.7月JDK1.8中HashMap的区别
在jdk1.7中HashMap主要结构为:数组+链表。
在jdk1.8中HashMap主要结构为:数组+链表+红黑树。
为什么要加入红黑树呢?学过的人都应该知道,红黑树查询是非常快的。
设想一个情况,当我们插入的Entry非常多时,我们的链表会长的可怕,这个时候去遍历链表寻找对应的key,所花费的时间可想而知的恐怖。
加入红黑树可以优化查询的时间,使查询效率快上不少。
那么在jdk1.8的HashMap中,当链表的长度超过8时,链表会自动转化为红黑树,优化查询速度。
同时还有一个区别:发生“hash冲突”时,是“头插法”,这是jdk1.7的做法,而在jdk1.8中,使用的是“尾插法”。
HashMap中的put(key,value)函数
public V put(K key, V value) {
if (key == null)
return putForNullKey(value);
/*如果key为null则调用 putForNullKey(value) 函数 这个函数先在table[0]这条链上找有没有key 为null的元素如果有就覆盖,如果没有就新建一个new一个key为null,value=value hash=0,的Entry放在table[0]。
*/
int hash = hash(key);
//获得key的hash值
int i = indexFor(hash, table.length);
//由hash值确定放在table表中的那一条链上。类似于取模后放在数组中的哪个位置。
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
Object k;
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
//如果链上原来有一个hash值相同,且key相同的则用新的value值进行覆盖。
}
}
//否则利用hash,key,value,new一个Entry对象插入到链表中。
modCount++;
addEntry(hash, key, value, i);
return null;
}
- HashMap中的key可以为 null ,此时hash=0,为什么key可以为null,因为HashMap中放的元素是Entry,而Entry包含了4个值(key,value,hash,next),key为 null 时不影响Entry映射到HashMap中。
- hash(key),产生一个正整数,这个整数与key相关。
- 用户插入的(key,value)对不是直接放到HashMap中的,而是用(key,value)以及后面由key value产生的hash,new一个Entry对象后再插入到HashMap中的。
- 如果对应的链上有一个hash值个key相同的Entry则覆盖value值,不new Entry对象,如果没有会先new 一个对象在将其插到对应的链上。(其中可能会涉及到扩充HashMap)。
HashMap中的get(Object key)函数
public V get(Object key) {
if (key == null)
return getForNullKey();
//如果key==null则在table[0]这条链上找,如果找到返回value值,否则返回null ,因为key==null的都是放在table[0]这条链上的。
Entry<K,V> entry = getEntry(key);
// getEntry(key)先key的hash值找到在数组的哪条链上,然后在链上查找key相同的如果没找到返回null
//如果找到了返回Entry的value值。
return null == entry ? null : entry.getValue();
}
private V getForNullKey() {
for (Entry<K,V> e = table[0]; e != null; e = e.next) {
if (e.key == null)
return e.value;
}
return null;
}
Entry<K,V> getEntry(Object key) {
int hash = (key == null) ? 0 : hash(key);
for (Entry<K,V> e = table[indexFor(hash, table.length)];
e != null;
e = e.next) {
Object k;
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
}
return null;
}
- 通过key来链表中查找元素包括两个过程,先由hash找到链(hash由key产生,不同的key可能产生相同的hash值,相同的hash值放在同一条链上),再用key在链上找。
- 如果key为null则只在table[0]和其链上查找,因为key为null都放在table[0]及其链上了。
- 因为在HashMap中查找到的是Entry对象,返回的值是Entry对象的value值。
想要线程安全的HashMap怎么办?
线程安全的Map的三种方法
| 方法 | 示例 | 原理 | 性能 |
|---|---|---|---|
| HashTable | Map<String, Object> map = new Hashtable<>(); | synchronized修饰get/put方法。方法级阻塞,只能同时一个线程操作get或put | 很差 |
| Collections.synchronizedMap | Map<String, Object> map =Collections.synchronizedMap(new HashMap<String, Object>()); | 所有方法都使用synchronized修饰 | 很差。和HashTable差不多 |
| JUC中的ConcurrentHashMap | Map<String, Object> map = new ConcurrentHashMap<>(); | 每次只给一个桶(数组项)加锁 | 很好 |
ConcurrentHashMap如何保证的线程安全?
JDK1.7:使用分段锁,将一个Map分为了16个段,每个段都是一个小的hashmap,每次操作只对其中一个段加锁
JDK1.8: 采用CAS+Synchronized保证线程安全,每次插入数据时判断在当前数组下标是否是第一次插入,是就通过CAS方式插入,然后判断f.hash是否=-1,是的话就说明其他线程正在进行扩容,当前线程也会参与扩容;删除方法用了synchronized修饰,保证并发下移除元素安全
总结:
1、ConcurrentHashMap在JDK1.7中使用的是数组加链表的结构,其中数组分两大类,大数组segment,小数组HashEntry,而加锁是通过给Segment加ReentrantLock重入锁来保证线程安全
2、ConcurrentHashMap在JDK1.8中使用的是数组加链表加红黑树的结构,它通过CAS或synchronized来保证线程安全的,并且缩小了锁的粒度,查询性能也更高
HashTable与HashMap的区别
- HashTable的每个方法都用synchronized修饰,因此是线程安全的,但同时读写效率很低
- HashMap的key可以使用null(但只能有一个),value可以为null,而HashTable都不允许存储key和value值为空的元素
- HashTable只对key进行一次hash,HashMap进行了两次Hash
- HashTable底层使用的数组加链表,HashMap在JDK1.8后是基于数组+链表+红黑树来实现的
- HashMap的初始容量为16,HashTable的初始容量为11
- HashMap的扩容机制为扩容两倍,而HashTable的扩容机制为两倍-1
- HashTable不会转换为红黑树