HashMap分析:
1.根据键的hashCode值存储数据,大多数情况下可以直接定位到它的值,因而具有很快的访问速度。
2.HashMap非线程安全(并发情况下put操作可能会引起死循环,因为多线程会导致HashMap的Entry链表形成环形数据结构,一旦形成环形,Entry的next节点永远不为空,就会产生死循环获取Entry)。
Hashtable分析:
1.使用synchronized来保证线程安全,但在线程竞争激烈的情况下Hashtable效率非常低下。
2.一个线程访问Hashtable的同步方法,其他线程也访问hashtable的同步方法时候,会进入阻塞或者轮询状态。
ConcurrentHashMap分析:
1.采用了锁分段技术。
2.将数据分成一段一段地存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据时候,其他段的数据也能被其他线程访问。
3.线程安全。
分析:
因为在并发编程中,使用HashMap可能导致程序死循环,而使用线程安全的Hashtable效率又非常低下,基于两个原因所以有了ConcurrentHashMap上场机会。
HashMap内部实现:
结构:数组 + 链表 + 红黑树(JDK1.8增加了红黑树部分)
部分Node源码:
/**
* Basic hash bin node, used for most entries. (See below for
* TreeNode subclass, and in LinkedHashMap for its Entry subclass.)
*/
static class Node<K,V> implements Map.Entry<K,V> {
final int hash; //定位数组索引位置
final K key;
V value;
Node<K,V> next; //链表的下一个node
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() {
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;
}
}
1.Node是HashMap的一个静态内部类,实现Map.Entry<K,V>接口,本质是一个映射(键值对)。
2.HashM安排使用哈希表来存储。哈希表为解决冲突,可以采用开放地址法(再散列法)、链地址法、再哈希法、建立公共溢出区。Java中的HashMap采用的链地址法。即数组加链表的结合。当数据hash后得到数组下标,把数据放在对应下标元素的链表上。
几个字段:
transient Node<K,V>[] table //哈希表 即由Node结点组成的一维数组
int threshold //记录当前容量下,最适合存放多少键值对(容量*负载因子)
final float loadFactory //负载因子 默认0.75
transient int modCount //记录操作数
transient int size //实际存在的键值对数量
1.Node[] table初始长度默认16,loadFactory为负载因子默认0.75,threshold是HashMap所能容纳的最大数据量的Node个数。threshold = 容量 * loadFactory = 16 * 0.75 = 12。超过这个数目就需resize扩容。负载因子是对空间和时间效率的一个平衡选择,除非特殊情况比如内存空间很多而对时间效率要求很高,可以降低负载因子的值。可以减少哈希冲突。threshold 总小于容量 size总不大于threshold。
2.size是HashMap中实际存在的键值对数量。
3.modCount字段主要用来记录HashMap内部结构发生变化的次数。(put键值对,若是覆盖则不属于结构变化)。
4.可能要出现拉链过长的情况,一旦拉链过长则会严重影响性能。所以JDK1.8对数据结构做了进一步的优化,引入红黑树。
5.当链表长度太长(默认超过8)时候,链表就转换为红黑树,利用红黑树快速增删改查的特点以提高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) {
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;
}
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
可以看出定位数组索引位置的过程为
(key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16) & table.length
put:
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
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) //判断table是否为null或空
n = (tab = resize()).length; //为空则扩容
if ((p = tab[i = (n - 1) & hash]) == null) //i = (n - 1) & hash]得到数组索引位置
tab[i] = newNode(hash, key, value, null); //tab[i]为空则直接新建节点添加
else { //tab[i]不为空
Node<K,V> e; K k;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k)))) //判断tab[i]首个元素是否和key一样
e = p; //一样则直接覆盖
else if (p instanceof TreeNode) //不一样则判断tab[i]是否是红黑树
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value); //是则在树中插入键值对
else { //不是 则遍历tab[i]
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 //长度大于8
treeifyBin(tab, hash); //把链表转红黑树 在红黑树中操作
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k)))) //key存在则直接覆盖
break;
p = e;
}
}
//put进已经存在的key 覆盖后 直接返回 即结构未发送变化 不记录(modeCount不++)
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:
扩容,即重新计算容量。方法是用一个新的数组代替旧数组。
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table; //旧数组
//旧数组的cap总容量和thr最佳容量
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;
int newCap, newThr = 0;
if (oldCap > 0) {
//就数组cap超过最大cap(2^30)
if (oldCap >= MAXIMUM_CAPACITY) {
//修改为int最大值(2^31-1)返回 以后不再扩容
threshold = Integer.MAX_VALUE;
return oldTab;
}
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
//cap thr扩大两倍
newThr = oldThr << 1; // double threshold
}
//oldCap为0,oldThr不为0,将newCap设为oldThr
else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;
else { //oldCap、oldThr两个都为0 // zero initial threshold signifies using defaults
newCap = DEFAULT_INITIAL_CAPACITY; //newCap默认值16
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); //newThr = 默认loadFactor * 默认cap
}
//判断newThr是否计算
if (newThr == 0) { //没有计算,用新容量和负载因子计算
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
//计算完后更新当前threshold
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) { //元素不为null 存起来
oldTab[j] = null; //旧表元素置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; //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;
}
原来:
新表:
可见,每当HashMap扩容的时候需要重新计算,并且用新数组代替旧数组,所以如果在使用中 知道HashMap需要存多少键值,最好在创建时指定容量,防止put时候进制扩容。
简单模拟一个HashMap(不扩容,不使用红黑树)
/**
* Map接口
* @param <K>
* @param <V>
*/
public interface Map<K,V> {
V put(K k, V v);
V get(K k);
int size();
interface Entry<K,V>{
K getKey();
V getValue();
}
}
/**
* describe:HashMap简单模拟
*
* @author gary
* @date 2019/01/20
*/
public class HashMap<K, V> implements Map<K, V> {
private static int DEFAULT_INITIAL_CAPACITY = 16;
private Entry<K, V>[] table = null;
private int size = 0;
public HashMap() {
table = new Entry[DEFAULT_INITIAL_CAPACITY];
}
public int hash(K k) {
int x = k.hashCode() % DEFAULT_INITIAL_CAPACITY;
return x >= 0 ? x : -x;
}
@Override
public V put(K k, V v) {
size ++;
int index = hash(k);
Entry<K, V> entry = table[index];
if (entry == null) {
table[index] = newEntry(k, v, null);
} else {
table[index] = newEntry(k, v, entry);
}
return table[index].getValue();
}
public Entry<K, V> newEntry(K k, V v, Entry<K, V> next) {
return new Entry<K, V>(k, v, next);
}
@Override
public V get(K k) {
int index = hash(k);
if (table[index] == null) {
return null;
}
return find(k, table[index]);
}
public V find(K k, Entry<K, V> entry) {
if (k == entry.getKey() || k.equals(entry.getKey())) {
return entry.getValue();
} else {
if (entry.next != null) {
return find(k, entry.next);
}
}
return null;
}
@Override
public int size() {
return size;
}
class Entry<K, V> implements Map.Entry<K, V> {
K k;
V v;
Entry<K, V> next;
public Entry(K k, V v, Entry<K, V> next) {
this.k = k;
this.v = v;
this.next = next;
}
@Override
public K getKey() {
return k;
}
@Override
public V getValue() {
return v;
}
}
}
/**
* describe:测试MyHashMap
*
* @author gary
* @date 2019/01/20
*/
public class TestForMyHashMap {
public static void main(String[] args) {
Map<String, Integer> map = new HashMap<String, Integer>();
long start = System.currentTimeMillis();
for (int i = 0; i < 10000; i++) {
map.put("gary" + i, 30);
}
for (int i = 0; i < 10000; i++) {
map.get("gary" + i);
}
long end = System.currentTimeMillis();
System.out.println("map大小:" + map.size() + "时间:" + (end -start));
}
}
结果为:
map大小:10000时间:62