前言

事情不太多时,会时不时去看项目中同事写的代码可以作个参考或者学习,个人觉得只有这样才能走的更远,抱着一副老子天下第一的态度最终只能是井底之蛙。前两篇写到关于断点传续的文章,还有一篇还未写出,后续会补上,这里我们穿插一篇文章,这是我看到同事写的代码中有ConcurrentDictionary这个类,之前并未接触过,就深入了解了一下,所以算是查漏补缺,基础拾遗吧,想要学习的这种劲头越有,你会发觉突然涌现的知识越多,学无止境!。

话题

本节的内容算是非常老的一个知识点,在.NET4.0中就已经出现,并且在园中已有园友作出了一定分析,为何我又拿出来讲呢?理由如下:

(1)没用到过,算是自己的一次切身学习。

(2)对比一下园友所述,我想我是否能讲的更加详尽呢?挑战一下。

(3)是否能够让读者理解的更加透彻呢?打不打脸不要紧,重要的是学习的过程和心得。

在.NET1.0中出现了HashTable这个类,此类不是线程安全的,后来为了线程安全又有了Hashtable.Synchronized,之前看到同事用Hashtable.Synchronized来进行实体类与数据库中的表进行映射,紧接着又看到别的项目中有同事用ConcurrentDictionary类来进行映射,一查资料又发现Hashtable.Synchronized并不是真正的线程安全,至此才引起我的疑惑,于是决定一探究竟, 园中已有大篇文章说ConcurrentDictionary类不是线程安全的。为什么说是线程不安全的呢?至少我们首先得知道什么是线程安全,看看其定义是怎样的。定义如下:

线程安全:如果你的代码所在的进程中有多个线程在同时运行,而这些线程可能会同时运行这段代码。如果每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,就是线程安全的。

一搜索线程安全比较统一的定义就是上述所给出的,园中大部分对于此类中的GetOrAdd或者AddOrUpdate参数含有委托的方法觉得是线程不安全的,我们上述也给出线程安全的定义,现在我们来看看其中之一。

        private static readonly ConcurrentDictionary<string, string> _dictionary
= new ConcurrentDictionary<string, string>(); public static void Main(string[] args)
{
var task1 = Task.Run(() => PrintValue("JeffckWang"));
var task2 = Task.Run(() => PrintValue("cnblogs"));
Task.WaitAll(task1, task2); PrintValue("JeffckyWang from cnblogs");
Console.ReadKey();
} public static void PrintValue(string valueToPrint)
{
var valueFound = _dictionary.GetOrAdd("key",
x =>
{
return valueToPrint;
});
Console.WriteLine(valueFound);
}

对于GetOrAdd方法它是怎样知道数据应该是添加还是获取呢?该方法描述如下:

TValue GetOrAdd(TKey key, Func<TKey, TValue> valueFactory);  

当给出指定键时,会去进行遍历若存在直接返回其值,若不存在此时会调用第二个参数也就是委托将运行,并将其添加到字典中,最终返回给调用者此键对应的值。

此时运行上述程序我们会得到如下二者之一的结果:

我们开启两个线程,上述运行结果不都是一样的么, 按照上述定义应该是线程安全才对啊,好了到了这里关于线程安全的定义我们应该消除以下两点才算是真正的线程安全。

(1)竞争条件

(2)死锁

那么问题来了,什么又是竞争条件呢?好吧,我是传说中的十万个什么。

就像女朋友说的哪有这么多为什么,我说的都是对的,不要问为什么,但对于这么严谨的事情,我们得实事求是,是不。竞争条件是软件或者系统中的一种行为,它的输出不会受到其他事件的影响而影响,若因事件受到影响,如果事件未发生则后果很严重,继而产生bug诺。 最常见的场景发生在当有两个线程同时共享一个变量时,一个线程在读这个变量,而另外一个变量同时在写这个变量。比如定义一个变量初始化为0,现在有两个线程共享此变量,此时有一个线程操作将其增加1,同时另外一个线程操作也将其增加1此时此时得到的结果将是1,而实际上我们期待的结果应该是2,所以为了解决竞争我们通过用锁机制来实现在多线程环境下的线程安全。

那么问题来了,什么是死锁呢?

至于死锁则不用多讲,死锁发生在多线程或者并发环境下,为了等待其他操作完成,但是其他操作一直迟迟未完成从而造成死锁情况。满足什么条件才会引起死锁呢?如下:

(1)互斥:只有进程在给定的时间内使用资源。

(2)占用并等待。

(3)不可抢先。

(4)循环等待。

到了这里我们通过对线程安全的理解明白一般为了线程安全都会加锁来进行处理,而在ConcurrentDictionary中参数含有委托的方法并未加锁,但是结果依然是一样的,至于未加锁说是为了出现其他不可预料的情况,依据我个人理解并非完全线程不安全,只是对于多线程环境下有可能出现数据不一致的情况,为什么说数据不一致呢?我们继续向下探讨。我们将上述方法进行修改如下:

        public static void PrintValue(string valueToPrint)
{
var valueFound = _dictionary.GetOrAdd("key",
x =>
{
Interlocked.Increment(ref _runCount);
Thread.Sleep();
return valueToPrint;
});
Console.WriteLine(valueFound);
}

主程序输出运行次数:

            var task1 = Task.Run(() => PrintValue("JeffckyWang"));
var task2 = Task.Run(() => PrintValue("cnblogs"));
Task.WaitAll(task1, task2); PrintValue("JeffckyWang from cnblogs"); Console.WriteLine(string.Format("运行次数为:{0}", _runCount));

此时我们看到确确实实获得了相同的值,但是却运行了两次,为什么会运行两次,此时第二个线程在运行调用之前,而第一个线程的值还未进行保存而导致。整个情况大致可以进行如下描述:

(1)线程1调用GetOrAdd方法时,此键不存在,此时会调用valueFactory这个委托。

(2)线程2也调用GetOrAdd方法,此时线程1还未完成,此时也会调用valueFactory这个委托。

(3)线程1完成调用,并返回JeffckyWang值到字典中,此时检查键还并未有值,然后将其添加到新的KeyValuePair中,并将JeffckyWang返回给调用者。

(4)线程2完成调用,并返回cnblogs值到字典中,此时检查此键的值已经被保存在线程1中,于是中断添加其值用线程1中的值进行代替,最终返回给调用者。

(5)线程3调用GetOrAdd方法找到键key其值已经存在,并返回其值给调用者,不再调用valueFactory这个委托。

从这里我们知道了结果是一致的,但是运行了两次,其上是三个线程,若是更多线程,则会重复运行多次,如此或造成数据不一致,所以我的理解是并非完全线程不安全。难道此类中的两个方法是线程不安全,.NET团队没意识到么,其实早就意识到了,上述也说明了如果为了防止出现意想不到的情况才这样设计,说到这里就需要多说两句,开源最大的好处就是能集思广益,目前已开源的 Microsoft.AspNetCore.Mvc.Core ,我们可以查看中间件管道源代码如下:

    /// <summary>
/// Builds a middleware pipeline after receiving the pipeline from a pipeline provider
/// </summary>
public class MiddlewareFilterBuilder
{
// 'GetOrAdd' call on the dictionary is not thread safe and we might end up creating the pipeline more
// once. To prevent this Lazy<> is used. In the worst case multiple Lazy<> objects are created for multiple
// threads but only one of the objects succeeds in creating a pipeline.
private readonly ConcurrentDictionary<Type, Lazy<RequestDelegate>> _pipelinesCache
= new ConcurrentDictionary<Type, Lazy<RequestDelegate>>();
private readonly MiddlewareFilterConfigurationProvider _configurationProvider; public IApplicationBuilder ApplicationBuilder { get; set; }
}

通过ConcurrentDictionary类调用上述方法无法保证委托调用的次数,在对于mvc中间管道只能初始化一次所以ASP.NET Core团队使用Lazy<>来初始化,此时我们将上述也进行上述对应的修改,如下:

               private static readonly ConcurrentDictionary<string, Lazy<string>> _lazyDictionary
= new ConcurrentDictionary<string, Lazy<string>>(); var valueFound = _lazyDictionary.GetOrAdd("key",
x => new Lazy<string>(
() =>
{
Interlocked.Increment(ref _runCount);
Thread.Sleep();
return valueToPrint;
}));
Console.WriteLine(valueFound.Value);

此时将得到如下:

我们将第二个参数修改为Lazy<string>,最终调用valueFound.value将调用次数输出到控制台上。此时我们再来解释上述整个过程发生了什么。

(1)线程1调用GetOrAdd方法时,此键不存在,此时会调用valueFactory这个委托。

(2)线程2也调用GetOrAdd方法,此时线程1还未完成,此时也会调用valueFactory这个委托。

(3)线程1完成调用,返回一个未初始化的Lazy<string>对象,此时在Lazy<string>对象上的委托还未进行调用,此时检查未存在键key的值,于是将Lazy<striing>插入到字典中,并返回给调用者。

(4)线程2也完成调用,此时返回一个未初始化的Lazy<string>对象,在此之前检查到已存在键key的值通过线程1被保存到了字典中,所以会中断创建,于是其值会被线程1中的值所代替并返回给调用者。

(5)线程1调用Lazy<string>.Value,委托的调用以线程安全的方式运行,所以如果被两个线程同时调用则只运行一次。

(6)线程2调用Lazy<string>.Value,此时相同的Lazy<string>刚被线程1初始化过,此时则不会再进行第二次委托调用,如果线程1的委托初始化还未完成,此时线程2将被阻塞,直到完成为止,线程2才进行调用。

(7)线程3调用GetOrAdd方法,此时已存在键key则不再调用委托,直接返回键key保存的结果给调用者。

上述使用Lazy来强迫我们运行委托只运行一次,如果调用委托比较耗时此时不利用Lazy来实现那么将调用多次,结果可想而知,现在我们只需要运行一次,虽然二者结果是一样的。我们通过调用Lazy<string>.Value来促使委托以线程安全的方式运行,从而保证在某一个时刻只有一个线程在运行,其他调用Lazy<string>.Value将会被阻塞直到第一个调用执行完,其余的线程将使用相同的结果。

那么问题来了调用Lazy<>.Value为何是线程安全的呢?

我们接下来看看Lazy对象。方便演示我们定义一个博客类

    public class Blog
{
public string BlogName { get; set; } public Blog()
{
Console.WriteLine("博客构造函数被调用");
BlogName = "JeffckyWang";
}
}

接下来在控制台进行调用:

            var blog = new Lazy<Blog>();
Console.WriteLine("博客对象被定义");
if (!blog.IsValueCreated) Console.WriteLine("博客对象还未被初始化");
Console.WriteLine("博客名称为:" + (blog.Value as Blog).BlogName);
if (blog.IsValueCreated)
Console.WriteLine("博客对象现在已经被初始化完毕");

打印如下:

通过上述打印我们知道当调用blog.Value时,此时博客对象才被创建并返回对象中的属性字段的值,上述布尔属性即IsValueCreated显示表明Lazy对象是否已经被初始化,上述初始化对象过程可以简述如下:

            var lazyBlog = new Lazy<Blog>
(
() =>
{
var blogObj = new Blog() { BlogName = "JeffckyWang" };
return blogObj;
}
);

打印结果和上述一致。上述运行都是在非线程安全的模式下进行,要是在多线程环境下对象只被创建一次我们需要用到如下构造函数:

 public Lazy(LazyThreadSafetyMode mode);
public Lazy(Func<T> valueFactory, LazyThreadSafetyMode mode);

通过指定LazyThreadSafetyMode的枚举值来进行。

(1)None = 0【线程不安全】

(2)PublicationOnly = 1【针对于多线程,有多个线程运行初始化方法时,当第一个线程完成时其值则会设置到其他线程】

(3)ExecutionAndPublication = 2【针对单线程,加锁机制,每个初始化方法执行完毕,其值则相应的输出】

我们演示下情况:

    public class Blog
{
public int BlogId { get; set; }
public Blog()
{
Console.WriteLine("博客构造函数被调用");
}
}
        static void Run(object obj)
{
var blogLazy = obj as Lazy<Blog>;
var blog = blogLazy.Value as Blog;
blog.BlogId++;
Thread.Sleep();
Console.WriteLine("博客Id为:" + blog.BlogId); }
            var lazyBlog = new Lazy<Blog>
(
() =>
{
var blogObj = new Blog() { BlogId = };
return blogObj;
}, LazyThreadSafetyMode.PublicationOnly
);
Console.WriteLine("博客对象被定义");
ThreadPool.QueueUserWorkItem(new WaitCallback(Run), lazyBlog);
ThreadPool.QueueUserWorkItem(new WaitCallback(Run), lazyBlog);

结果打印如下:

奇怪的是当改变线程安全模式为 LazyThreadSafetyMode.ExecutionAndPublication 时结果应该为101和102才是,居然返回的都是102,但是将上述blog.BogId++和暂停时间顺序颠倒时如下:

  Thread.Sleep();
blog.BlogId++;

此时两个模式返回的都是101和102,不知是何缘故!上述在ConcurrentDictionary类中为了两个方法能保证线程安全我们利用Lazy来实现,默认的模式为 LazyThreadSafetyMode.ExecutionAndPublication 保证委托只执行一次。为了不破坏原生调用ConcurrentDictionary的GetOrAdd方法,但是又为了保证线程安全,我们封装一个方法来方便进行调用。

        public class LazyConcurrentDictionary<TKey, TValue>
{
private readonly ConcurrentDictionary<TKey, Lazy<TValue>> concurrentDictionary; public LazyConcurrentDictionary()
{
this.concurrentDictionary = new ConcurrentDictionary<TKey, Lazy<TValue>>();
} public TValue GetOrAdd(TKey key, Func<TKey, TValue> valueFactory)
{
var lazyResult = this.concurrentDictionary.GetOrAdd(key, k => new Lazy<TValue>(() => valueFactory(k), LazyThreadSafetyMode.ExecutionAndPublication)); return lazyResult.Value;
}
}

原封不动的进行方法调用:

        private static int _runCount = ;
private static readonly LazyConcurrentDictionary<string, string> _lazyDictionary
= new LazyConcurrentDictionary<string, string>(); public static void Main(string[] args)
{
var task1 = Task.Run(() => PrintValue("JeffckyWang"));
var task2 = Task.Run(() => PrintValue("cnblogs"));
Task.WaitAll(task1, task2); PrintValue("JeffckyWang from cnblogs");
Console.WriteLine(string.Format("运行次数为:{0}", _runCount));
Console.Read();
} public static void PrintValue(string valueToPrint)
{
var valueFound = _lazyDictionary.GetOrAdd("key",
x => {
Interlocked.Increment(ref _runCount);
Thread.Sleep();
return valueToPrint;
});
Console.WriteLine(valueFound);
}

最终正确打印只运行一次的结果,如下:

总结

本节我们学习了ConcurrentDictionary类里面有两个方法严格来说非线程安全,但是也可以得到相同的结果,若我们仅仅只是得到相同的结果且操作不是太耗时其实完全可以忽略这一点,若当利用ConcurrentDictionary类中的此二者方法来做比较耗时的操作,此时就要注意让其线程安全利用Lazy来保证其只能执行一次,所以对ConcurrentDictionary来说并非所有情况都要实现严格意义上的线程安全,根据实际场景而定才是最佳解决方案。时不时多看看别人写的代码,涨涨见识,每天积累一点,日子长了就牛逼了!

ConcurrentDictionary线程不安全么,你难道没疑惑,你难道弄懂了么?的更多相关文章

  1. 一文弄懂-Netty核心功能及线程模型

    目录 一. Netty是什么? 二. Netty 的使用场景 三. Netty通讯示例 1. Netty的maven依赖 2. 服务端代码 3. 客户端代码 四. Netty线程模型 五. Netty ...

  2. 不是吧!做了两年java还没弄懂JVM堆?进来看看你就明白了

    堆的核心概述 一个JVM实例只存在一个堆内存,堆也是java内存管理的核心区域Java堆区在jvm启动的时候被创建,其空间大小也就确定了.是jvm管理的最大一块内存空间.(堆内存的大小可以调节)< ...

  3. 线程池系列二:一张动图,彻底懂了execute和submit

    ​ 我们知道线程池通过execute方法执行提交的Runnable任务,但Runnable只是执行任务,没有返回任何信息. [线程池原理:线程池原来是个外包公司,打工人我悟了] 若是我们想在异步执行完 ...

  4. 没人比我更懂 HashMap :)

    哈,标题开个玩笑,0202 年的段子哈. 一.首先看一下 HashMap 的构造函数 /** * Constructs an empty <tt>HashMap</tt> wi ...

  5. 关于ConcurrentDictionary的线程安全

    ConcurrentDictionary是.net BCL的一个线程安全的字典类,由于其方法的线程安全性,使用时无需手动加锁,被广泛应用于多线程编程中.然而,有的时候他们并不是如我们预期的那样工作. ...

  6. 性能追击:万字长文30+图揭秘8大主流服务器程序线程模型 | Node.js,Apache,Nginx,Netty,Redis,Tomcat,MySQL,Zuul

    本文为<高性能网络编程游记>的第六篇"性能追击:万字长文30+图揭秘8大主流服务器程序线程模型". 最近拍的照片比较少,不知道配什么图好,于是自己画了一个,凑合着用,让 ...

  7. 一篇关于Python装饰器的博文

    这是一篇关于python装饰器的博文 在学习python的过程中处处受阻,之前的学习中Python的装饰器学习了好几遍也没能真正的弄懂.这一次抓住视频猛啃了一波,就连python大佬讲解装饰器起来也需 ...

  8. 安装jdk的时候为什么会有两个jre文件

    有些东西,你懂不懂其实并不太影响你干活,但有些人就是有疑惑就非得弄懂,不然浑身难受,我大概就是这种德性的.昨天安装javaSE的时候,看到jdk中有个jre文件夹,而根目录下又有个jre文件夹,非常困 ...

  9. JavaScript回调函数的实现

    https://github.com/forsigner/blog/blob/master/source/_posts/javascript-callback.md 在JavaScript中,回调函数 ...

随机推荐

  1. Tomcat7 配置 ssl

    运行一个配置了ssl的项目时tomcat总是启动不成功,报错:“requires the APR/native library which is not available”,后来发现是找不到apr的 ...

  2. Where product development should start

    We all need to know our customers in order to create products they’ll actually buy. This is why the  ...

  3. 解决overflow:hidden在安卓微信页面没有效果的办法

    在做h5移动端时候,发现overflow: hidden;在安卓微信页面失效问题,经研究和实验,用第三种方法和第四种方法可以解决! 1.完全隐藏 在<boby>里加入scroll=&quo ...

  4. 【OAuth2.0】Spring Security OAuth2.0篇之初识

    不吐不快 因为项目需求开始接触OAuth2.0授权协议.断断续续接触了有两周左右的时间.不得不吐槽的,依然是自己的学习习惯问题,总是着急想了解一切,习惯性地钻牛角尖去理解小的细节,而不是从宏观上去掌握 ...

  5. 使用Aspose.Cells读取Excel

      最新更新请访问: http://denghejun.github.io Aspose.Cells读取Excel非常方便,以下是一个简单的实现读取和导出Excel的操作类: 以下是Aspose.Ce ...

  6. PHP的FastCGI

    CGI全称是“通用网关接口”(Common Gateway Interface), 它可以让一个客户端,从网页浏览器向执行在Web服务器上的程序请求数据. CGI描述了客户端和这个程序之间传输数据的一 ...

  7. CDN解决方案

    帝联cdn 这算不算是打广告呢?

  8. .NET不可变集合已经正式发布

    微软基础类库(Base Class Library)团队已经完成了.NET不可变集合的正式版本,但不包括ImmutableArray.与其一起发布的还包括针对其它不可变对象类型的设计指南. 如果你需要 ...

  9. 玩转JavaScript OOP[1]——复杂类型

    概述 在JavaScript中,我们可以使用函数.数组.对象,以及日期.正则等一些内置类型的实例,它们都是复杂类型的表现.从本质上讲,这些复杂类型都是Object类型.本篇将主要介绍三种Object类 ...

  10. Hadoop学习笔记—14.ZooKeeper环境搭建

    从字面上来看,ZooKeeper表示动物园管理员,这是一个十分奇妙的名字,我们又想起了Hadoop生态系统中,许多项目的Logo都采用了动物,比如Hadoop采用了大象的形象,所以我们可以猜测ZooK ...