面试常问道会问到JUC,常见的就是CHM了,来说一下CHM不同版本的方法操作
JDK7的CHM的底层实现是分段segment数组实现的,segment里面包含HashEntry,每个HashEntry又组成一个链表。
常用方法get方法:
- 首先获取segment的锁;
- 然后进行根据Hash算法定位到具体的桶的位置;
- 如果桶不为空的话,并且根据判断第一个结点就是需要插入的位置,直接替换旧值;
- 否则的话逐个进行遍历寻找,不存在冲突的话创建一个新结点进行头插,
- 如果桶为空的话,进行创建结点直接插入;
- 之后进行判断当前结点数是否超过限制值,超过进行扩容
- 然后进行基数+1,
- 最后需要进行释放锁,因为segment继承Lock锁,防止阻塞产生。
//这是Segment中的 put 方法
final V put(K key, int hash, V value, boolean onlyIfAbsent) {
//此处实现需要回去segment的锁
HashEntry<K,V> node = tryLock() ? null :
scanAndLockForPut(key, hash, value);
V oldValue;
try {
//当前Segment的table数组
HashEntry<K,V>[] tab = table;
//进行Hash计算,找到其所在HashEntry数组的下标
int index = (tab.length - 1) & hash;
//当前下标位置的第一个HashEntry节点
HashEntry<K,V> first = entryAt(tab, index);
for (HashEntry<K,V> e = first;;) {
//如果第一个节点不为空
if (e != null) {
K k;
//如果第一个节点,就是要插入的节点,则替换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;
}
//如果当前位置没有节点
//或者当前链表已经遍历完了还没找到相等的key
else {
//如果不为空,则直接头插
if (node != null)
node.setNext(first);
//否则,创建一个新的结点,并头插
else
node = new HashEntry<K,V>(hash, key, value, first);
int c = count + 1;
//如果当前Segment中的元素大于阈值,并且tab长度没有超过容量最大值,则扩容
if (c > threshold && tab.length < MAXIMUM_CAPACITY)
rehash(node);
else
setEntryAt(tab, index, node);
++modCount;
//更新count值
count = c;
oldValue = null;
break;
}
}
} finally {
//ReentrantLock必须手动解锁
unlock();
}
return oldValue;
}
}
get方法:
相对就很简单
- 根据Hash计算出segment的位置
- 判断是否为空,判断链表是否为空,然后进行遍历链表
- 根据查找到返回,没有返回null即可
由于 HashEntry 中的 value 属性是用 volatile 关键词修饰的,保证了内存可见性,所以每次获取时都是最新值。
ConcurrentHashMap 的 get 方法是非常高效的,因为整个过程都不需要加锁。
public V get(Object key) {
Segment<K,V> s;
HashEntry<K,V>[] tab;
//计算hash值
int h = hash(key);
//先定位到 key 所在的Segment
long u = (((h >>> segmentShift) & segmentMask) << SSHIFT) + SBASE;
if ((s = (Segment<K,V>)UNSAFE.getObjectVolatile(segments, u)) != null &&
(tab = s.table) != null) {
//若Segment不为空,且链表也不为空,则遍历查找节点
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;
//找到则返回它的 value 值,否则返回 null
if ((k = e.key) == key || (e.hash == h && key.equals(k)))
return e.value;
}
}
return null;
}
JDK8的常用方法put
在JDK8的CHM已经变得和原来不同了,数据结构实现是Node数组+链表+红黑树进行实现,没有再使用分段锁,并发而是使用优化后的synchronized+CAS。
put方法的步骤:
final V putVal(K key, V value, boolean onlyIfAbsent) {
//首先可以看见的就是不允许存放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;
//判断CHM是否为空
if (tab == null || (n = tab.length) == 0)
//只有一个线程可以进行初始化
tab = initTable();
//若已经初始化,根据Hash计算index,看table[index]是否为空
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
//为空的话,进行CAS插入一个新的结点,并且只有一个结点成功
if (casTabAt(tab, i, null,
new Node<K,V>(hash, key, value, null)))
break;
}
//桶不为空,判断结点Hash是否为MOVDE(-1),是否进行数组扩容
else if ((fh = f.hash) == MOVED)
tab = helpTransfer(tab, f);
else {
V oldVal = null;
//否则进行加锁保证线程安全
synchronized (f) {
if (tabAt(tab, i) == f) {
if (fh >= 0) {
binCount = 1;
//进行遍历,寻找是否有产生冲突key的结点,则直接替换结点值
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) {
//判断结点数是否大于等于8 否则进行红黑树转换
if (binCount >= TREEIFY_THRESHOLD)
treeifyBin(tab, i);
if (oldVal != null)
return oldVal;
break;
}
}
}
//无异常的话进行结点数增加
addCount(1L, binCount);
return null;
}
get方法:
- 根据key计算Hash值
- table不为空且找到具体的桶,进行遍历查找
- 否则进行树的遍历
- 否则进行链表进行遍历查询 有就返回,否则返回null
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;
}
再有就是JDK7计算size的方式:
- 使用乐观的方式进行计算,假定计算的时候没有修改segment结构的操作,但是如果发生了就进行重试,两次重试失败的话就会进行锁住所有的segment,然后再进行计算size,已确定准确的大小。