ConcurrentDictionary<T,V> 的这两个操作不是原子性的
好久不见,马甲哥封闭居家半个月,记录之前遇到的一件小事。
ConcurrentDictionary<TKey,TValue>绝大部分api都是线程安全且原子性的,
唯二的例外是接收工厂委托的api:AddOrUpdate
、GetOrAdd
,这两个api不是原子性的,需要引起重视。
All these operations are atomic and are thread-safe with regards to all other operations on the ConcurrentDictionary<TKey,TValue> class. The only exceptions are the methods that accept a delegate, that is, AddOrUpdate and GetOrAdd.
之前有个同事就因为这个case背了一个P。
AddOrUpdate(TKey, TValue, Func<TKey,TValue,TValue> valueFactory);
GetOrAdd(TKey key, Func<TKey, TValue> valueFactory);
(注意,包括其他接收工厂委托的重载函数)
Q1: valueFactory工厂函数不在锁定范围,为什么不在锁范围?
A: 还不是因为微软不相信你能写出健壮的业务代码,未知的业务代码可能造成死锁。
However, delegates for these methods are called outside the locks to avoid the problems that can arise from executing unknown code under a lock. Therefore, the code executed by these delegates is not subject to the atomicity of the operation.
Q2:带来的效果?
- valueFactory工厂函数可能会多次执行
- 虽然会多次执行, 但插入的值永远是一个,插入的值取决于哪个线程率先插入字典。
Q3: 怎么做到的?
A: 源代码做了double check了,后续线程通过工厂类创建值后,会再次检查字典,发现已有值,会丢弃自己创建的值。
示例代码:
using System.Collections.Concurrent;
public class Program
{
private static int _runCount = 0;
private static readonly ConcurrentDictionary<string, string> _dictionary
= new ConcurrentDictionary<string, string>();
public static void Main(string[] args)
{
var task1 = Task.Run(() => PrintValue("The first value"));
var task2 = Task.Run(() => PrintValue("The second value"));
var task3 = Task.Run(() => PrintValue("The three value"));
var task4 = Task.Run(() => PrintValue("The four value"));
Task.WaitAll(task1, task2, task4,task4);
PrintValue("The five value");
Console.WriteLine($"Run count: {_runCount}");
}
public static void PrintValue(string valueToPrint)
{
var valueFound = _dictionary.GetOrAdd("key",
x =>
{
Interlocked.Increment(ref _runCount);
Thread.Sleep(100);
return valueToPrint;
});
Console.WriteLine(valueFound);
}
}
上面4个线程并发插入字典,每次随机输出,_runCount=4
显示工厂类执行4次。
Q4:如果工厂产值的代价很大,不允许多次创建,如何实现?
笔者的同事之前就遇到这样的问题,高并发请求频繁创建redis连接,直接打挂了机器。
A: 有一个trick能解决这个问题: valueFactory工厂函数返回Lazy容器.
using System.Collections.Concurrent;
public class Program
{
private static int _runCount2 = 0;
private static readonly ConcurrentDictionary<string, Lazy<string>> _lazyDictionary
= new ConcurrentDictionary<string, Lazy<string>>();
public static void Main(string[] args)
{
task1 = Task.Run(() => PrintValueLazy("The first value"));
task2 = Task.Run(() => PrintValueLazy("The second value"));
task3 = Task.Run(() => PrintValueLazy("The three value"));
task4 = Task.Run(() => PrintValueLazy("The four value"));
Task.WaitAll(task1, task2, task4, task4);
PrintValue("The five value");
Console.WriteLine($"Run count: {_runCount2}");
}
public static void PrintValueLazy(string valueToPrint)
{
var valueFound = _lazyDictionary.GetOrAdd("key",
x => new Lazy<string>(
() =>
{
Interlocked.Increment(ref _runCount2);
Thread.Sleep(100);
return valueToPrint;
}));
Console.WriteLine(valueFound.Value);
}
}
上面示例,依旧会稳定随机输出,但是_runOut=1
表明产值动作只执行了一次、
valueFactory工厂函数返回Lazy容器是一个精妙的trick。
① 工厂函数依旧没进入锁定过程,会多次执行;
② 与最上面的例子类似,只会插入一个Lazy容器(后续线程依旧做double check发现字典key已经有Lazy容器了,会放弃插入);
③ 线程执行Lazy.Value, 这时才会执行创建value的工厂函数;
④ 多个线程尝试执行Lazy.Value, 但这个延迟初始化方式被设置为ExecutionAndPublication
:
不仅以线程安全的方式执行, 而且确保只会执行一次构造函数。
public Lazy(Func<T> valueFactory)
:this(valueFactory, LazyThreadSafetyMode.ExecutionAndPublication, useDefaultConstructor: false)
{
}
控制构造函数执行的枚举值 | 描述 |
---|---|
ExecutionAndPublication | 能确保只有一个线程能够以线程安全方式执行构造函数 |
None | 线程不安全 |
Publication | 并发线程都会执行初始化函数,以先完成初始化的值为准 |
IHttpClientFactory
在构建<命名HttpClient,活跃连接Handler>字典时, 也用到了这个技巧,大家自行欣赏DefaultHttpCLientFactory源码。
总结
为解决ConcurrentDictionary GetOrAdd(key, valueFactory) 工厂函数在并发场景下被多次执行的问题。
① valueFactory工厂函数产生Lazy容器
② 将Lazy容器的值初始化姿势设定为ExecutionAndPublication
(线程安全且执行一次)。
两姿势缺一不可。
ConcurrentDictionary<T,V> 的这两个操作不是原子性的的更多相关文章
- dpkg: error: -i (--install) 和 -i (--install) 两个操作之间有矛盾
1 错误描述 youhaidong@youhaidong-ThinkPad-Edge-E545:~$ sudo dpkg -i -i WineQQ2013-20131120-Longene.deb [ ...
- 【spring data jpa】使用spring data jpa时,关于service层一个方法中进行【删除】和【插入】两种操作在同一个事务内处理
场景: 现在有这么一个情况,就是在service中提供的一个方法是先将符合条件的数据全部删除,然后再将新的条件全部插入数据库中 这个场景需要保证service中执行两步 1.删除 2.插入 这两步自然 ...
- MySQL Index--NOT IN和不等于两类操作无法走索引?
经常被问,NOT IN和<>操作就无法走索引? 真想只有一个:具体问题具体分析,没有前提的问题都是耍流氓. 准备测试数据: ## 删除测试表 DROP TABLE IF EXISTS tb ...
- sk_buff整理笔记(两、操作函数)
承接上一:sk_buff 整理笔记(一.数据结构)这一篇要讲的是内核为sk_buff结构提供的一些操作函数. 第一.首先要讲的是sk_buff中的四大指针: 四大指针各自是:head.data.tai ...
- [转载]redis持久化的两种操作RDB和AOF
Redis 持久化: 提供了多种不同级别的持久化方式:一种是RDB,另一种是AOF. RDB 持久化可以在指定的时间间隔内生成数据集的时间点快照(point-in-time snapshot). AO ...
- selectAll, unSelectAll两个操作的实现
private void updateBatchSelectionStatus() { ContactListAdapter.ViewHolder viewHolder = null; ...
- std::vector 两种操作的比较
swap assign 这里只想说明这三种操作的用处和效率.swap和assign都可以用在将一个vector的内容全部复制给另外一个vector,区别是swap会改变源vector,而assign会 ...
- Hive的两种操作模式
Hive的客户端操作 Hive的客户端操作 通过JDBC操作Hive 通过Thrift操作Hive 通过JDBC操作Hive 首先 Hive 启动远程服务 hive --service hiveser ...
- 栈(Stack)和队列(Queue)是两种操作受限的线性表。
(线性表:线性表是一种线性结构,它是一个含有n≥0个结点的有限序列,同一个线性表中的数据元素数据类型相同并且满足"一对一"的逻辑关系. "一对一"的逻辑关系指的 ...
随机推荐
- python各种BUG报错解决
报错1 python学习交流群:660193417### Could not build atari-py: Command '['cmake', '..']' returned non-zero e ...
- 校验日期格式为yyyy-MM-dd
/** * 校验时间 * * @param text * @return */ public static boolean checkTime(String text) { DateFormat fo ...
- Java + Selenium + OpenCV解决自动化测试中的滑块验证
最近工作过程中,一个常用的被测网站突然增加了滑块验证环节,导致整个自动化项目失效了. 为了解决这个滑块验证问题,在网上查阅了一些资料后,总结并实现了解决方案,现记录如下. 1.滑块验证思路 被测对象的 ...
- 为你的网站加上live2d的动态小挂件,博君一晒
原文转载自「刘悦的技术博客」https://v3u.cn/a_id_122 喜欢二次元的朋友一定对大名鼎鼎的live2d技术并不陌生,live2D是一种应用于电子游戏的绘图渲染技术,技术由日本Cybe ...
- 王霸雄图荣华敝屣,谈笑间尽归尘土|基于Python3双队列数据结构搭建股票/外汇交易匹配撮合系统
原文转载自「刘悦的技术博客」https://v3u.cn/a_id_192 如果你爱他,那么送他去股市,因为那里是天堂:如果你恨他,送他去股市,因为那里是地狱. 在过去的一年里,新冠疫情持续冲击世界经 ...
- 6.22 NOI 模拟
\(T1\)递归 给出 \(Thue-Morse\) 序列的定义三 每次 \(0\rightarrow 01\),\(1\rightarrow 10\) \(0\rightarrow 01 \righ ...
- Windows环境下部署MySQL主从并模拟升级到8.0
GreatSQL社区原创内容未经授权不得随意使用,转载请联系小编并注明来源. 一.部署实例 1.下载2个软件 http://ftp.kaist.ac.kr/mysql/Downloads/MySQL- ...
- mybatis 05: mybatis中的动态代理
mybatis中动态代理存在的意义 图示 图示分析 分层说明:界面层为第一层,业务逻辑层(接口层 + 实现层)为第二层,数据访问层(接口层 + 实现层)为第三层 业务逻辑层和数据访问层:分别分两层来开 ...
- 倒计时0日!Apache DolphineScheduler4月 Meetup 大佬手把手教你大数据开发,离线调度
随着互联网技术和信息技术的发展,信息的数据化产生了许多无法用常规工具量化.处理和捕捉的数字信息.面对多元的数据类型,海量的信息价值,如何有效地对大数据进行挖掘分析,对大数据工作流进行调度,是保障企业大 ...
- JVM 配置参数 -D,-X,-XX 的区别
转载请注明出处: 最近在安全护网行动,需要针对服务进行不断的安全加固,如 对服务的 log4j 的安全配置进行防护,对 fastjson 的漏洞进行安全加固等,最快的防护方法就是通过在服务启动的时候, ...