注:本文所述为JDK1.8版本
HashMap和ConcurrentHashMap源码分析
1、HashMap
1.1 概述
HashMap底层是基于数组+链表+红黑树组成的。
数据结构示例图:
主要属性:
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
static final int MAXIMUM_CAPACITY = 1 << 30;
static final float DEFAULT_LOAD_FACTOR = 0.75f;
// 用于判断是否需要将链表转换为红黑树的阈值
static final int TREEIFY_THRESHOLD = 8;
// JDK1.7中的HashEntry修改为Node
transient Node<K,V>[] table;
transient Set<Map.Entry<K,V>> entrySet;
transient int size;
TreeNode源码:
1.2 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;
//判断当前桶是否为空,空的话就需要初始化(resize中会判断是否需要初始化)
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
// 根据当前key的hashcode定位到具体的桶中并判断是否为空,为空表明没有Hash冲突就直接在当前位置创建一个新桶即可
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e; K k;
// 如果当前桶有值(Hash冲突),那么就要比较当前桶中的key、key的hashcode与写入的key是否相等,相等就赋值给e,后面统一进行赋值及返回
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 {
// 如果是个链表,就需要将当前的key、value封装成一个新节点写入当前桶的后面(采用尾插法)
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;
}
// 如果在遍历链表的过程中,找到key相同时直接退出遍历
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
// 如果e != null就相当于存在相同的key,那就需要将值覆盖
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;
}
1.3 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;
// 将key hash之后取得所定位的桶
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {
// 如果桶不为空
// 判断桶的第一个位置的key是否为查询的key,是就直接返回value
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 {
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
return null;
}
链表修改为红黑树之后查询效率变为了O(logn)
1.4 hash算法
key为空,hash值为0;h为key的hashCode,结果就变为了 h^(h>>>16),即:高16位不变,低位高位异或运算。
1.5 HashMap存在的问题
在JDK1.7中,HashMap在并发执行put操作时会引起死循环,是因为多线程会导致HashMap的Entry链表形成环形数据结构,一旦形成环形数据结构,Entry的next节点永不为空,就会产生死循环获取Entry
原因就是JDK1.7链表插入新节点采用的是头插法,这样在线程一扩容迁移元素时,会将元素顺序改变,导致两个线程中出现元素的相互指向而形成循环链表,JDK1.8采用了尾插法,从根源上杜绝了这种情况的发生。
1.6 遍历方式
Iterator<Map.Entry<String, Integer>> entryIterator = map.entrySet().iterator();
while (entryIterator.hasNext()) {
Map.Entry<String, Integer> next = entryIterator.next();
System.out.println("key=" + next.getKey() + " value=" + next.getValue());
}
Iterator<String> iterator = map.keySet().iterator();
while (iterator.hasNext()) {
String key = iterator.next();
System.out.println("key=" + key + " value=" + map.get(key));
}
建议使用第一种EntrySet进行遍历,第一种可以把key和value同时取出,第二种还需要通过key取一次value,效率较低
2、ConcurrentHashMap
2.1概述
- HashMap线程不安全
- HashTable效率低下:HashTable容器使用synchronized来保证线程安全,但在线程竞争激烈的情况下HashTable的效率非常低下。因为当一个线程访问HashTable的同步方法,其他线程也访问HashTable的同步方法时,会进入阻塞或轮询状态
结构图:
JDK1.8中抛弃了原有的Segment分段锁,而采用了CAS+synchronized来保证并发安全性。
Node源码:
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
volatile V val;
volatile Node<K,V> next;
Node(int hash, K key, V val, Node<K,V> next) {
this.hash = hash;
this.key = key;
this.val = val;
this.next = next;
}
public final K getKey() { return key; }
public final V getValue() { return val; }
public final int hashCode() { return key.hashCode() ^ val.hashCode(); }
public final String toString(){ return key + "=" + val; }
public final V setValue(V value) {
throw new UnsupportedOperationException();
}
也将1.7中存放数据的HashEntry改为了Node,但作用都是相同的
其中的val和next都用了volatile修饰,保证了可见性
TreeNode源码:
2.2 put方法
public V put(K key, V value) {
return putVal(key, value, false);
}
final V putVal(K key, V value, boolean onlyIfAbsent) {
if (key == null || value == null) throw new NullPointerException();
// 根据key计算出hashcode
int hash = spread(key.hashCode());
int binCount = 0;
for (Node<K,V>[] tab = table;;) {
Node<K,V> f; int n, i, fh;
// 判断是否需要进行初始化
if (tab == null || (n = tab.length) == 0)
tab = initTable();
// f为当前key定位出的Node,如果为空表示当前位置可以写入数据,利用CAS尝试写入,失败则自旋保证成功
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
if (casTabAt(tab, i, null,
new Node<K,V>(hash, key, value, null)))
break;
}
// 如果当前位置的hashcode==MOVED==-1,则需要进行扩容
else if ((fh = f.hash) == MOVED)
tab = helpTransfer(tab, f);
else {
V oldVal = null;
// 如果都不满足,则利用synchronized锁写入数据
synchronized (f) {
if (tabAt(tab, i) == f) {
if (fh >= 0) {
binCount = 1;
for (Node<K,V> e = f;; ++binCount) {
K ek;
if (e.hash == hash &&
((ek = e.key) == key ||
(ek != null && key.equals(ek)))) {
oldVal = e.val;
if (!onlyIfAbsent)
e.val = value;
break;
}
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) {
// 如果数量大于TREEIFY_THRESHOLD则要转换为红黑树
if (binCount >= TREEIFY_THRESHOLD)
treeifyBin(tab, i);
if (oldVal != null)
return oldVal;
break;
}
}
}
addCount(1L, binCount);
return null;
}
2.3 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;
}
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;
}
}
return null;
}
根据计算出来的 hashcode 寻址,如果就在桶上那么直接返回值。如果是红黑树那就按照树的方式获取值。都不满足那就按照链表的方式遍历获取值
JDK1.8在1.7的数据结构上做了大的改动,采用红黑树之后可以保证查询效率O(logn),甚至取消了ReentrantLock改为了synchronized,这样可以看出在新版的JDK中对synchronized优化是很到位的
2.4 hash算法
ConcurrentHashMap中key不能为空,其中h参数为key的hashCode;与HashMap相比,是HashMap的hash结果再与上int的最大值作为ConcurrentHashMap的hash值。
- 7fffffff是8位16进制
- 每个16进制代表4个bit
- 8✖4bit=32bit=4Byte
- f的二进制为:1111,7的二进制位0111
- int类型的长度位4Byte
- 左边起,第一位为符号位,0代表正数,1代表负数
- 0x7fffffff代表int的最大值
3、小结:HashMap和ConcurrentHashMap的异同
- HashMap允许null值null键,而ConcurrentHashMap则不允许null值null键
- HashMap是非线程安全的,而ConcurrentHashMap是线程安全的
- HashMap和ConcurrentHashMap的底层都是数组+链表+红黑树