Map集合
Map用于保存具有映射关系的数据,以键值对的形式存储,支持通过key来访问value,因此key不能重复。
Map接口下主要有Hashtable
、HashMap
、LinkedHashMap
、ConcurrentHashMap
四个主要的实现类,实现的基本原理类似,主要详解HashMap
和ConcurrentHashMap
1. HashMap
使用频率最高的键值存储集合,底层采用数组+链表+红黑树存储数据 (JDK1.8) 。访问速度快,线程不安全,最多允许一个key为null
,允许多个value为null
hashCode()
对key求哈希值,然后根据哈希值
和数组长度-1
做与运算
得到数组下标位置,数组中存储的值存放的是key value组成的结点,如果两个两个key不同但是哈希值相同,就在对应数组位置创建一个链表,如果链表长度超过8,此时数组长度 >= 64 链表转为红黑树,否则扩容数组。(详见哈希冲突和哈希函数)
在JDK1.7中HashMap
底层采用数组+链表实现,且链表采用头插法,在多线程并发环境下rehash扩容容易造成死循环 (详见 Java HashMap的死循环)
如果创建是不指定大小 HashMap
默认的初始化大小为 16。之后每次扩充,容量变为原来的 2 倍,如果指定大小
HashMap
会将其扩充为 2 的幂次方大小(为了方便求数组下标)
指定容量和负载因子的构造函数(负载因子是 哈希表中键值对 / 数组容量 0.75是时间和空间的综合考量)
// 默认容量 16
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
// 最大容量
static final int MAXIMUM_CAPACITY = 1 << 30;
//默认负载因子 0.75
static final float DEFAULT_LOAD_FACTOR = 0.75f;
// 有参构造函数
public HashMap(int initialCapacity, float loadFactor) {
// 初始化容量<0 报非法参数异常
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
// 大于最大容量就初始化为最大容量
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
// 负载因子<=0 或为NaN 报非法参数异常
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
this.loadFactor = loadFactor;
this.threshold = tableSizeFor(initialCapacity);
}
tableSizeFor
方法保证容量是 2 的幂次方大小
static final int tableSizeFor(int cap) {
int n = cap - 1;
n |= n >>> 1;
n |= n >>> 2;
n |= n >>> 4;
n |= n >>> 8;
n |= n >>> 16;
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}
put
过程
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
// onlyIfAbsent参数如果是true,那么只有不存在该key时才会进行put操作,默认为false可以覆盖
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
// 第一次put的时,通过resize()创建数组并初始化数组长度
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
// 寻找数组下标,如果该位置没有值,直接初始化一个结点,将key value存入结点,并把结点放入数组
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
// 数组该位置已经存在数据
Node<K,V> e; K k;
// 首先,判断该位置第一个数据与待put的key是否equals相等,相等就取出这个结点
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);
// TREEIFY_THRESHOLD为8,如果新增元素是第8个尝试转化红黑树
if (binCount >= TREEIFY_THRESHOLD - 1)
treeifyBin(tab, hash);
break;
}
// 在链表中 找到了equals相等的key,取出这个结点
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
// 结点不为null说明存在equals相等的key,需要进行value覆盖 并返回旧值
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
// 如果新put的元素导致size超过了阈值,需要扩容
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
resize()
方法用于初始化数组或数组扩容,如果是扩容,扩容后容量为原来的 2 倍
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 &&
((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 {
// 遍历链表查找key
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
// 没有找到就返回null
return null;
}
2. Hashtable
Hashtable
与HashMap
类似,底层采用数组+链表实现。不过Hashtable
是线程安全的,通过加synchronized
关键字保证,不允许key和value为null
,在并发性上不如ConcurrentHashMap
为什么Hashtable效率低
因为synchronized
关键字对整个对象进行加锁,锁住了Hash表
3. ConcurrentHashMap
与HashMap
类似 唯一区别在于ConcurrentHashMap
是线程安全的 key和value不能为null
JDK1.7,底层采用分段的数组+链表 ,使用分段锁保证线程安全。
ConcurrentHashMap
在对象中保存了一个Segment
数组,将整个Hash表划分为多个分段(默认16段);而每个Segment
元素,即每个分段则类似于一个Hashtable
。在执行put操作
时首先根据hash算法
定位到元素属于哪个Segment
,然后对该Segment
加锁而不影响其他Segment
。因此,ConcurrentHashMap
在多线程并发编程中可是实现多线程put操作
。Segment
通过继承 ReentrantLock
来进行加锁
JDK1.8,ConcurrentHashMap
底层采用数组+链表+红黑树,加锁采用CAS
和synchronized
实现。锁粒度更细,synchronized
只锁当前链表
或红黑树的首节点
,这样只要 hash值
不冲突,就不会发生并发,不会影响其他 Node
结点 的读写,效率大幅提升。
put()
过程
public V put(K key, V value) {
return putVal(key, value, false);
}
final V putVal(K key, V value, boolean onlyIfAbsent) {
// key和value不能为null
if (key == null || value == null) throw new NullPointerException();
// 计算hash值
int hash = spread(key.hashCode());
// 记录链表的长度
int binCount = 0;
for (Node<K,V>[] tab = table;;) {
Node<K,V> f; int n, i, fh;
// 数组为null或空 就初始化数组
if (tab == null || (n = tab.length) == 0)
tab = initTable();
// 找到hash值对应的数组下标,下标的第一个结点f
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
// 如果数组该位置为空,通过一次CAS操作将这个值放入 结束
// 如果CAS失败,说明有并发,循环进行下一次CAS操作
if (casTabAt(tab, i, null,
new Node<K,V>(hash, key, value, null)))
break;
}
// 需要扩容
else if ((fh = f.hash) == MOVED)
tab = helpTransfer(tab, f);
// f为该位置的头结点,且不为null
else {
V oldVal = null;
// 获取头结点的监视器锁
synchronized (f) {
if (tabAt(tab, i) == f) {
// 头结点的hash值>=0 说明是链表
if (fh >= 0) {
//累计链表的长度
binCount = 1;
// 遍历链表
for (Node<K,V> e = f;; ++binCount) {
K ek;
// 如果有equals相等的key,判断是否进行值覆盖 随后break
if (e.hash == hash &&
((ek = e.key) == key ||
(ek != null && key.equals(ek)))) {
oldVal = e.val;
if (!onlyIfAbsent)
e.val = value;
break;
}
// 链表中不存在该key,插入链表尾部
Node<K,V> pred = e;
if ((e = e.next) == null) {
pred.next = new Node<K,V>(hash, key,
value, null);
break;
}
}
}
// 红黑树
else if (f instanceof TreeBin) {
Node<K,V> p;
binCount = 2;
// 往红黑树中插入新节点
if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
value)) != null) {
oldVal = p.val;
if (!onlyIfAbsent)
p.val = value;
}
}
}
}
if (binCount != 0) {
// 判断是否将链表转化为红黑树,临界值为8
// 如果数组长度小于64 选择扩容数组而不转红黑树
if (binCount >= TREEIFY_THRESHOLD)
treeifyBin(tab, i);
if (oldVal != null)
return oldVal;
break;
}
}
}
addCount(1L, binCount);
return null;
}
get()
过程
public V get(Object key) {
Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
// 计算哈希值
int h = spread(key.hashCode());
// 找数组对应位置
if ((tab = table) != null && (n = tab.length) > 0 &&
(e = tabAt(tab, (n - 1) & h)) != null) {
if ((eh = e.hash) == h) {
if ((ek = e.key) == key || (ek != null && key.equals(ek)))
return e.val;
}
// 如果头结点的 hash值 < 0 说明 正在扩容/该位置为红黑树
else if (eh < 0)
return (p = e.find(h, key)) != null ? p.val : null;
// 遍历链表查找
while ((e = e.next) != null) {
if (e.hash == h &&
((ek = e.key) == key || (ek != null && key.equals(ek))))
return e.val;
}
}
// 不存在 返回null
return null;
}