ConcurrentDictionary线程不安全么,你难道没疑惑,你难道弄懂了么?

前言

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

话题

本节的内容算是很是老的一个知识点,在.NET4.0中就已经出现,而且在园中已有园友做出了必定分析,为什么我又拿出来说呢?理由以下:安全

(1)没用到过,算是本身的一次切身学习。多线程

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

(3)是否可以让读者理解的更加透彻呢?打不打脸没关系,重要的是学习的过程和心得。mvc

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

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

一搜索线程安全比较统一的定义就是上述所给出的,园中大部分对于此类中的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方法它是怎样知道数据应该是添加仍是获取呢?该方法描述以下:ui

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

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

此时运行上述程序咱们会获得以下两者之一的结果:

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

(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(100);
                       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(100);
                        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(100);
            Console.WriteLine("博客Id为:" + blog.BlogId);

        }
复制代码
复制代码
            var lazyBlog = new Lazy<Blog>
            (
                () =>
                {
                    var blogObj = new Blog() { BlogId = 100 };
                    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(100);          
  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 = 0;
        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(100); return valueToPrint; }); Console.WriteLine(valueFound); }
复制代码

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

总结

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