ConcurrentHashMap源码分析
jdk1.7
数据结构
1.7的时候底层是由segments数组+HashEntry数组+链表
组成的,在1.8的时候又不同。
// segments数组
final Segment<K,V>[] segments;
static final class Segment<K,V> extends ReentrantLock implements Serializable {
// table数组 和 HashMap的table差不多,是拿来真正存放数据的
transient volatile HashEntry<K,V>[] table;
// table中的元素个数
transient int count;
transient int modCount;
// 阈值
transient int threshold;
// 负载因子
final float loadFactor;
.......
.......
}
HashEntry和HashMap中的Entry差不多,不同的地方是,用了volatile
去修饰Value
和next
。
static final class HashEntry<K,V> {
final int hash;
final K key;
// 使用 volatile修饰
volatile V value;
volatile HashEntry<K,V> next;
.......
......
}
volatile
保证不同线程对这个变量进行实时操作的可见性(实现可见性
)和禁止指令重排序(实现有序性
)。
put操作
public V put(K key, V value) {
Segment<K,V> s;
// 不允许value值为null,如果为null就抛空指针异常
if (value == null)
throw new NullPointerException();
// 获取hash
int hash = hash(key);
int j = (hash >>> segmentShift) & segmentMask;
if ((s = (Segment<K,V>)UNSAFE.getObject // nonvolatile; recheck
(segments, (j << SSHIFT) + SBASE)) == null) // in ensureSegment
s = ensureSegment(j);
// 调用下方的put方法
return s.put(key, hash, value, false);
}
上面👆的是先找到对应的segment,再进行下面👇的put操作
final V put(K key, int hash, V value, boolean onlyIfAbsent) {
// 尝试获取锁,如果获取失败则scanAndLockForPut(...)自旋获取锁
HashEntry<K,V> node = tryLock() ? null :
scanAndLockForPut(key, hash, value);
V oldValue;
try {
// 获取当前segment的table数组
HashEntry<K,V>[] tab = table;
// 计算对应的数组下标index
int index = (tab.length - 1) & hash;
// 获取对应下标的HashEntry
HashEntry<K,V> first = entryAt(tab, index);
// 遍历该HashEntry
for (HashEntry<K,V> e = first;;) {
// 如果不为空
if (e != null) {
K k;
// 判断传入的 key 和当前遍历的 key 是否相等,相等则覆盖旧的 value。
if ((k = e.key) == key ||
(e.hash == hash && key.equals(k))) {
oldValue = e.value;
if (!onlyIfAbsent) {
e.value = value;
++modCount;
}
break;
}
e = e.next;
}
else {
// 为空则需要新建一个 HashEntry 并加入到 Segment 中,同时会先判断是否需要扩容。
if (node != null)
node.setNext(first);
else
node = new HashEntry<K,V>(hash, key, value, first);
int c = count + 1;
if (c > threshold && tab.length < MAXIMUM_CAPACITY)
rehash(node);
else
setEntryAt(tab, index, node);
++modCount;
count = c;
oldValue = null;
break;
}
}
} finally {
unlock();
}
return oldValue;
}
get操作
public V get(Object key) {
Segment<K,V> s; // manually integrate access methods to reduce overhead
HashEntry<K,V>[] tab;
// 计算key对应的hash
int h = hash(key);
long u = (((h >>> segmentShift) & segmentMask) << SSHIFT) + SBASE;
// 通过hash获取到具体的segment
if ((s = (Segment<K,V>)UNSAFE.getObjectVolatile(segments, u)) != null &&
(tab = s.table) != null) {
// 通过hash获取到具体HashEntry,并遍历该HashEntry
for (HashEntry<K,V> e = (HashEntry<K,V>) UNSAFE.getObjectVolatile
(tab, ((long)(((tab.length - 1) & h)) << TSHIFT) + TBASE);
e != null; e = e.next) {
K k;
// 传入的key和当前遍历的key相等,返回对应的value
if ((k = e.key) == key || (e.hash == h && key.equals(k)))
return e.value;
}
}
return null;
}
1. 将key通过hash后定位到具体的segment,然后再通过hash定位到具体的HashEntry
2. 遍历该HashEntry,查找和传入key相等的HashEntry.key,返回该HashEntry的value。
因为 HashEntry 中的 value 属性是用 volatile 关键词修饰的,保证了内存可见性,所以每次获取时都是最新值。
ConcurrentHashMap 的 get 方法是非常高效的,因为整个过程都不需要加锁
。
jdk1.8
数据结构
抛弃了原有的 Segment 分段锁,而采用了 CAS + synchronized 来保证并发安全性。
和HashMap类似,将HashMap改成了Node,引入了红黑树
数据结构为:数组+链表/红黑树,当链表数量>=8的时候,转换为红黑树,当数量<=6时,红黑树转换为链表
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();
// 根据key.hashCode获取进行散列后的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();
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
// 当前 key 定位出的 Node,如果为空表示当前位置可以写入数据,利用 CAS 尝试写入,失败则自旋保证成功。
if (casTabAt(tab, i, null,
new Node<K,V>(hash, key, value, null)))
break; // no lock when adding to empty bin
}
// 如果当前位置的 hashcode == MOVED == -1,则需要进行扩容。
else if ((fh = f.hash) == MOVED)
tab = helpTransfer(tab, f);
// 如果都不满足,则利用synchronized锁写入
else {
V oldVal = null;
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;
}
文字步骤:
-
根据 key 计算出 hashcode 。
-
判断是否需要进行初始化。
-
为当前 key 定位出的 Node,如果为空表示当前位置可以写入数据,利用 CAS 尝试写入,失败则自旋保证成功。
-
如果当前位置的 hashcode == MOVED == -1,则需要进行扩容。
-
如果都不满足,则利用 synchronized 锁写入数据。
-
如果数量大于 TREEIFY_THRESHOLD 则要转换为红黑树。
get操作
public V get(Object key) {
Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
// 将key.hashCode()进行散列化
int h = spread(key.hashCode());
// 定位该hash对应table数组中的Node
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;
}