本章接着上两章,链接直达:
死磕 java集合之ConcurrentHashMap源码分析(一)
死磕 java集合之ConcurrentHashMap源码分析(二)
删除元素
删除元素跟添加元素一样,都是先找到元素所在的桶,然后采用分段锁的思想锁住整个桶,再进行操作。
public V remove(Object key) {
// 调用替换节点方法
return replaceNode(key, null, null);
}
final V replaceNode(Object key, V value, Object cv) {
// 计算hash
int hash = spread(key.hashCode());
// 自旋
for (Node<K,V>[] tab = table;;) {
Node<K,V> f; int n, i, fh;
if (tab == null || (n = tab.length) == 0 ||
(f = tabAt(tab, i = (n - 1) & hash)) == null)
// 如果目标key所在的桶不存在,跳出循环返回null
break;
else if ((fh = f.hash) == MOVED)
// 如果正在扩容中,协助扩容
tab = helpTransfer(tab, f);
else {
V oldVal = null;
// 标记是否处理过
boolean validated = false;
synchronized (f) {
// 再次验证当前桶第一个元素是否被修改过
if (tabAt(tab, i) == f) {
if (fh >= 0) {
// fh>=0表示是链表节点
validated = true;
// 遍历链表寻找目标节点
for (Node<K,V> e = f, pred = null;;) {
K ek;
if (e.hash == hash &&
((ek = e.key) == key ||
(ek != null && key.equals(ek)))) {
// 找到了目标节点
V ev = e.val;
// 检查目标节点旧value是否等于cv
if (cv == null || cv == ev ||
(ev != null && cv.equals(ev))) {
oldVal = ev;
if (value != null)
// 如果value不为空则替换旧值
e.val = value;
else if (pred != null)
// 如果前置节点不为空
// 删除当前节点
pred.next = e.next;
else
// 如果前置节点为空
// 说明是桶中第一个元素,删除之
setTabAt(tab, i, e.next);
}
break;
}
pred = e;
// 遍历到链表尾部还没找到元素,跳出循环
if ((e = e.next) == null)
break;
}
}
else if (f instanceof TreeBin) {
// 如果是树节点
validated = true;
TreeBin<K,V> t = (TreeBin<K,V>)f;
TreeNode<K,V> r, p;
// 遍历树找到了目标节点
if ((r = t.root) != null &&
(p = r.findTreeNode(hash, key, null)) != null) {
V pv = p.val;
// 检查目标节点旧value是否等于cv
if (cv == null || cv == pv ||
(pv != null && cv.equals(pv))) {
oldVal = pv;
if (value != null)
// 如果value不为空则替换旧值
p.val = value;
else if (t.removeTreeNode(p))
// 如果value为空则删除元素
// 如果删除后树的元素个数较少则退化成链表
// t.removeTreeNode(p)这个方法返回true表示删除节点后树的元素个数较少
setTabAt(tab, i, untreeify(t.first));
}
}
}
}
}
// 如果处理过,不管有没有找到元素都返回
if (validated) {
// 如果找到了元素,返回其旧值
if (oldVal != null) {
// 如果要替换的值为空,元素个数减1
if (value == null)
addCount(-1L, -1);
return oldVal;
}
break;
}
}
}
// 没找到元素返回空
return null;
}
(1)计算hash;
(2)如果所在的桶不存在,表示没有找到目标元素,返回;
(3)如果正在扩容,则协助扩容完成后再进行删除操作;
(4)如果是以链表形式存储的,则遍历整个链表查找元素,找到之后再删除;
(5)如果是以树形式存储的,则遍历树查找元素,找到之后再删除;
(6)如果是以树形式存储的,删除元素之后树较小,则退化成链表;
(7)如果确实删除了元素,则整个map元素个数减1,并返回旧值;
(8)如果没有删除元素,则返回null;
获取元素
获取元素,根据目标key所在桶的第一个元素的不同采用不同的方式获取元素,关键点在于find()方法的重写。
public V get(Object key) {
Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
// 计算hash
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)
// hash小于0,说明是树或者正在扩容
// 使用find寻找元素,find的寻找方式依据Node的不同子类有不同的实现方式
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;
}
(1)hash到元素所在的桶;
(2)如果桶中第一个元素就是该找的元素,直接返回;
(3)如果是树或者正在迁移元素,则调用各自Node子类的find()方法寻找元素;
(4)如果是链表,遍历整个链表寻找元素;
(5)获取元素没有加锁;
获取元素个数
元素个数的存储也是采用分段的思想,获取元素个数时需要把所有段加起来。
public int size() {
// 调用sumCount()计算元素个数
long n = sumCount();
return ((n < 0L) ? 0 :
(n > (long)Integer.MAX_VALUE) ? Integer.MAX_VALUE :
(int)n);
}
final long sumCount() {
// 计算CounterCell所有段及baseCount的数量之和
CounterCell[] as = counterCells; CounterCell a;
long sum = baseCount;
if (as != null) {
for (int i = 0; i < as.length; ++i) {
if ((a = as[i]) != null)
sum += a.value;
}
}
return sum;
}
(1)元素的个数依据不同的线程存在在不同的段里;(见addCounter()分析)
(2)计算CounterCell所有段及baseCount的数量之和;
(3)获取元素个数没有加锁;
总结
(1)ConcurrentHashMap是HashMap的线程安全版本;
(2)ConcurrentHashMap采用(数组 + 链表 + 红黑树)的结构存储元素;
(3)ConcurrentHashMap相比于同样线程安全的HashTable,效率要高很多;
(4)ConcurrentHashMap采用的锁有 synchronized,CAS,自旋锁,分段锁,volatile等;
(5)ConcurrentHashMap中没有threshold和loadFactor这两个字段,而是采用sizeCtl来控制;
(6)sizeCtl = -1,表示正在进行初始化;
(7)sizeCtl = 0,默认值,表示后续在真正初始化的时候使用默认容量;
(8)sizeCtl > 0,在初始化之前存储的是传入的容量,在初始化或扩容后存储的是下一次的扩容门槛;
(9)sizeCtl = (resizeStamp << 16) + (1 + nThreads),表示正在进行扩容,高位存储扩容邮戳,低位存储扩容线程数加1;
(10)更新操作时如果正在进行扩容,当前线程协助扩容;
(11)更新操作会采用synchronized锁住当前桶的第一个元素,这是分段锁的思想;
(12)整个扩容过程都是通过CAS控制sizeCtl这个字段来进行的,这很关键;
(13)迁移完元素的桶会放置一个ForwardingNode节点,以标识该桶迁移完毕;
(14)元素个数的存储也是采用的分段思想,类似于LongAdder的实现;
(15)元素个数的更新会把不同的线程hash到不同的段上,减少资源争用;
(16)元素个数的更新如果还是出现多个线程同时更新一个段,则会扩容段(CounterCell);
(17)获取元素个数是把所有的段(包括baseCount和CounterCell)相加起来得到的;
(18)查询操作是不会加锁的,所以ConcurrentHashMap不是强一致性的;
(19)ConcurrentHashMap中不能存储key或value为null的元素;
彩蛋——值得学习的技术
ConcurrentHashMap中有哪些值得学习的技术呢?
我认为有以下几点:
(1)CAS + 自旋,乐观锁的思想,减少线程上下文切换的时间;
(2)分段锁的思想,减少同一把锁争用带来的低效问题;
(3)CounterCell,分段存储元素个数,减少多线程同时更新一个字段带来的低效;
(4)@sun.misc.Contended(CounterCell上的注解),避免伪共享;(p.s.伪共享我们后面也会讲的^^)
(5)多线程协同进行扩容;
(6)你又学到了哪些呢?
彩蛋——不能解决的问题
ConcurrentHashMap不能解决什么问题呢?
请看下面的例子:
private static final Map<Integer, Integer> map = new ConcurrentHashMap<>();
public void unsafeUpdate(Integer key, Integer value) {
Integer oldValue = map.get(key);
if (oldValue == null) {
map.put(key, value);
}
}
这里如果有多个线程同时调用unsafeUpdate()这个方法,ConcurrentHashMap还能保证线程安全吗?
答案是不能。因为get()之后if之前可能有其它线程已经put()了这个元素,这时候再put()就把那个线程put()的元素覆盖了。
那怎么修改呢?
答案也很简单,使用putIfAbsent()方法,它会保证元素不存在时才插入元素,如下:
public void safeUpdate(Integer key, Integer value) {
map.putIfAbsent(key, value);
}
那么,如果上面oldValue不是跟null比较,而是跟一个特定的值比如1进行比较怎么办?也就是下面这样:
public void unsafeUpdate(Integer key, Integer value) {
Integer oldValue = map.get(key);
if (oldValue == 1) {
map.put(key, value);
}
}
这样的话就没办法使用putIfAbsent()方法了。
其实,ConcurrentHashMap还提供了另一个方法叫replace(K key, V oldValue, V newValue)可以解决这个问题。
replace(K key, V oldValue, V newValue)这个方法可不能乱用,如果传入的newValue是null,则会删除元素。
public void safeUpdate(Integer key, Integer value) {
map.replace(key, 1, value);
}
那么,如果if之后不是简单的put()操作,而是还有其它业务操作,之后才是put(),比如下面这样,这该怎么办呢?
public void unsafeUpdate(Integer key, Integer value) {
Integer oldValue = map.get(key);
if (oldValue == 1) {
System.out.println(System.currentTimeMillis());
/**
* 其它业务操作
*/
System.out.println(System.currentTimeMillis());
map.put(key, value);
}
}
这时候就没办法使用ConcurrentHashMap提供的方法了,只能业务自己来保证线程安全了,比如下面这样:
public void safeUpdate(Integer key, Integer value) {
synchronized (map) {
Integer oldValue = map.get(key);
if (oldValue == null) {
System.out.println(System.currentTimeMillis());
/**
* 其它业务操作
*/
System.out.println(System.currentTimeMillis());
map.put(key, value);
}
}
}
这样虽然不太友好,但是最起码能保证业务逻辑是正确的。
当然,这里使用ConcurrentHashMap的意义也就不大了,可以换成普通的HashMap了。
上面只是举一个简单的例子,我们不能听说ConcurrentHashMap是线程安全的,就认为它无论什么情况下都是线程安全的,还是那句话尽信书不如无书。
这也正是我们读源码的目的之一,了解其本质,才能在我们的实际工作中少挖坑,不论是挖给别人还是挖给自己^^。
死磕 java集合之ConcurrentHashMap源码分析(三)的更多相关文章
-
死磕 java集合之ConcurrentHashMap源码分析(一)
开篇问题 (1)ConcurrentHashMap与HashMap的数据结构是否一样? (2)HashMap在多线程环境下何时会出现并发安全问题? (3)ConcurrentHashMap是怎么解决并 ...
-
死磕 java集合之ConcurrentHashMap源码分析(二)——扩容
本章接着上一章,链接直达请点我. 初始化桶数组 第一次放元素时,初始化桶数组. private final Node<K,V>[] initTable() { Node<K,V> ...
-
死磕 java集合之CopyOnWriteArraySet源码分析——内含巧妙设计
问题 (1)CopyOnWriteArraySet是用Map实现的吗? (2)CopyOnWriteArraySet是有序的吗? (3)CopyOnWriteArraySet是并发安全的吗? (4)C ...
-
死磕 java集合之DelayQueue源码分析
问题 (1)DelayQueue是阻塞队列吗? (2)DelayQueue的实现方式? (3)DelayQueue主要用于什么场景? 简介 DelayQueue是java并发包下的延时阻塞队列,常用于 ...
-
死磕 java集合之PriorityBlockingQueue源码分析
问题 (1)PriorityBlockingQueue的实现方式? (2)PriorityBlockingQueue是否需要扩容? (3)PriorityBlockingQueue是怎么控制并发安全的 ...
-
死磕 java集合之PriorityQueue源码分析
问题 (1)什么是优先级队列? (2)怎么实现一个优先级队列? (3)PriorityQueue是线程安全的吗? (4)PriorityQueue就有序的吗? 简介 优先级队列,是0个或多个元素的集合 ...
-
死磕 java集合之LinkedHashSet源码分析
问题 (1)LinkedHashSet的底层使用什么存储元素? (2)LinkedHashSet与HashSet有什么不同? (3)LinkedHashSet是有序的吗? (4)LinkedHashS ...
-
死磕 java集合之ArrayDeque源码分析
问题 (1)什么是双端队列? (2)ArrayDeque是怎么实现双端队列的? (3)ArrayDeque是线程安全的吗? (4)ArrayDeque是有界的吗? 简介 双端队列是一种特殊的队列,它的 ...
-
【死磕 Java 集合】— ConcurrentSkipListMap源码分析
转自:http://cmsblogs.com/?p=4773 [隐藏目录] 前情提要 简介 存储结构 源码分析 主要内部类 构造方法 添加元素 添加元素举例 删除元素 删除元素举例 查找元素 查找元素 ...
随机推荐
-
各大主流.Net的IOC框架性能测试比较
Autofac下载地址:http://code.google.com/p/autofac/ Castle Windsor下载地址:http://sourceforge.net/projects/cas ...
-
tyvj1614 魔塔魔塔!
描述 百度noip贴吧管理组开发了一个小游戏,叫魔塔魔塔.虽然把魔塔重复了两次,但其实还只是个魔塔而已,还是简化版的.游戏在一个N*M大小的地图中进行,每一格都是正方形.对于某一格,有若干种可能的状态 ...
-
LINQ技巧:如何通过多次调用GroupBy实现分组嵌套
问题如上,解决如下,目标在最下面:结果: using System; using System.Linq; using System.Collections.Generic; namespace Co ...
-
【Java Web】Eclipse中配置Marven环境
1 Marven简介 Apache Maven,是一个软件(特别是Java软件)项目管理及自动构建工具,由Apache软件基金会所提供.基于项目对象模型(缩写:POM)概念,Maven利用一个*信 ...
-
通过jsonp解决浏览器的跨域共享
因为浏览器的同源策略,普通ajax访问跨域请求返回的json数据是不会被浏览器接受的.看下面例子可以看出是访问不到的 首先 定义webapi 后台代码 public class JsopControl ...
-
2D命令行小游戏Beta1.0
前提: 遇到许多问题,没有参考大佬一些方法是敲不出来的...Orz using System; using System.Collections.Generic; using System.Linq; ...
-
vue项目中关于axios的简单使用
axios介绍 Axios 是一个基于 promise 的 HTTP 库,可以用在浏览器和 node.js 中 官方仓库:https://github.com/axios/axios 中文文档:htt ...
-
彻底搞懂 C# 的 async/await
前言 Talk is cheap, Show you the code first! private void button1_Click(object sender, EventArgs e) { ...
-
CF 219D 树形DP
CF 219D [题目链接]CF 219D [题目类型]树形DP &题意: 给一个n节点的有向无环图,要找一个这样的点:该点到其它n-1要逆转的道路最少,(边<u,v>,如果v要到 ...
-
背水一战 Windows 10 (119) - 后台任务: 后台下载任务(任务分组,组完成后触发后台任务)
[源码下载] 背水一战 Windows 10 (119) - 后台任务: 后台下载任务(任务分组,组完成后触发后台任务) 作者:webabcd 介绍背水一战 Windows 10 之 后台任务 后台下 ...