zoukankan      html  css  js  c++  java
  • 线程安全——你忽视了么?

    好久没写blog了,今天还是想写一下关于线程安全的问题。从我以前的blog中可以清楚的知道,我是比较反对使用singleton模式的。这里我只是想举一个非常简单的例子来说明singleton带来的问题很可能比我们想想的要严重的多。

    话说我反对使用singleton的主要原因是,singleton的提供者通常无法很好实现线程安全,要么对线程安全的认知,要么干脆认为线程安全什么的无关紧要。

    那么一个线程不怎么安全的代码到底会出现写什么问题那?


    例子1——Random

    先来看看这段代码:

     1 using System;
     2 using System.Threading;
     3 
     4 namespace NotThreadSafe
     5 {
     6     class Program
     7     {
     8         static volatile bool s_running = true;
     9         static CountdownEvent s_event = new CountdownEvent(2);
    10         static Random s_random = new Random();
    11 
    12         static void Main(string[] args)
    13         {
    14             ThreadPool.QueueUserWorkItem(DoRandomHeavily);
    15             ThreadPool.QueueUserWorkItem(DoRandomHeavily);
    16             Thread.Sleep(1000);
    17             s_running = false;
    18             s_event.Wait();
    19             Console.WriteLine(s_random.Next());
    20         }
    21 
    22         static void DoRandomHeavily(object _)
    23         {
    24             while (s_running)
    25                 s_random.Next();
    26             s_event.Signal();
    27         }
    28     }
    29 }
    View Code

    那么大家来猜猜看,这段代码运行后的输出是什么。

    我估计大部分人会说结果在0~int.MaxValue之间的任何一个数,但是实际上,这段代码如果在多核电脑上跑出来的结果(至少是>99.9%的概率)是0。是不是比较意外?


    Why?!

    来看看为什么结果是如此的诡异,首先,请查阅msdn,msdn上清楚的说明了Random类型不保证实例成员的线程安全,因此,想上面代码中那样使用random是不正确的,会导致一些问题。

    有很多人认为random的作用本来就就是随便给个数,线程不安全也无所谓,不还是随便给个数。

    不过.net把random设计成一个可以通过seed重复生成某个序列的类,虽然这个序列看起来很random,实际上还是一个算法的,有算法不是问题,问题是这个算法依赖于一些实例字段。通过反编译,可以发现random有三个重要的实力字段:一个int[]用于存储一堆数字;两个int,用于决定取数组中的那两个数来做一系列操作(核心是一个减法)。

    显然读取两个int的字段和写入两个int的字段都不是原子的,因此在多线程环境中,很可能会出现读取第一个int字段的指是来自当前cpu本地的缓存值,而在读取第二个int字段之前,当前cpu的缓存刷新了,读取出来的值变成来自其他cpu写入的值。虽然这看起来确实改变了random应有的序列,但是还不至于影响到我们的目的——随机。但是别忘了,这个改变可以导致两个int字段的数值是相同的情况,而且这个概率已经大到在数学上还不能被称为小概率!

    一旦两个int字段变成了相同的值,那么噩梦就开始了,从数组的相同位置取出两次数,那么在>99%的概率下是相同的(<1%的是遇到多线程问题,算你狗屎运。。。),两数相减得0,做一系列运算,还是0,再存入数组,一圈转下来,只要中间不发什么多线程问题之类的,数组就全被刷成了0,之后的两个int在这么随便指都无所谓了,因为0减0一定是0。


    还不过瘾?例子2-Dictionary

    来看下代码:

     1 using System;
     2 using System.Collections.Generic;
     3 using System.Threading;
     4 
     5 namespace NotThreadSafe
     6 {
     7     class Program
     8     {
     9         static volatile bool s_running = true;
    10         static CountdownEvent s_event = new CountdownEvent(2);
    11         static Dictionary<int, int> s_dict = new Dictionary<int, int>();
    12 
    13         static void Main(string[] args)
    14         {
    15             ThreadPool.QueueUserWorkItem(DoItHeavily);
    16             ThreadPool.QueueUserWorkItem(DoItHeavily);
    17             Thread.Sleep(1000);
    18             s_running = false;
    19             s_event.Wait();
    20             Console.WriteLine(s_dict.Count);
    21         }
    22 
    23         static void DoItHeavily(object _)
    24         {
    25             while (s_running)
    26             {
    27                 s_dict[1] = 1;
    28                 s_dict.Remove(1);
    29             }
    30             s_event.Signal();
    31         }
    32     }
    33 }
    View Code

    继续问,大家认为会输出什么?

    没看懂上面的人估计会说0,看懂了上面的人也许会说程序报错崩溃,不过事实总是让人意外,在多核心机上跑出的结果通常是程序无法退出,并且直接占用两个核的计算资源(即:双核是100%占用,4核是50%占用...)

    为什么,简单的说就是:Dictionary的内部数据结构被多线程破坏,导致在Add时直接陷入了死循环。如果想听复杂的解释,不妨自己去抓下dump。


    更多的例子我就不举了,回到msdn查一下线程安全,不难发现Framework为我们提供的99%的类型都写着不保证实例成员的线程安全,几乎只有个别类型会写着线程安全,而且这里面的大部分还是那些用于处理线程安全的锁类型,那么singleton的提供者们,请看下代码确实线程安全了么?该加锁的都加了么?如果保证不了线程安全,那么只要是多线程环境,外加使用的足够heavy,1秒内就可以使代码瞬间崩塌。

    PS:代码部分以.net为例,但是别认为只有.net有这个问题哦,所有基于线程编程的oop语言都会有这个问题,根据我对部分java的程序员的了解,大部分还是很喜欢拿着spring直接注入一个对象,方式么——singleton,问原因,省内存啊。。。

  • 相关阅读:
    html5---音频视频基础一
    SQL--数据库性能优化详解
    微信--入门开发
    MVC----基础
    jquery -----简单分页
    sql优化
    集合的总结
    java并发编程与高并发解决方案
    java中boolean类型占几个字节
    正则表达式小结
  • 原文地址:https://www.cnblogs.com/vwxyzh/p/3087829.html
Copyright © 2011-2022 走看看