确保真正的线程安全——微软为什么不提供线程安全库

简介:

线程安全在高并发情况下是一个非常严重的问题。以下代码在多线程访问时,会出现问题。我们以List.Add为例,来说明在多线程访问下的状况。以下代码是List.Add的实现。


 
 
public   void  Add(T item) {      if  ( this ._size  ==   this ._items.Length)  this .EnsureCapacity( this ._size  +   1 );      this ._items[ this ._size ++ =  item;      this ._version ++ ; }

当两个线程同时访问一个List的Add方法时,这个方法的第一条指令就可能出现不一致性了。因为,此时两个线程访问时_size都是一样的,正确情况下List应该执行EnsureCapacity(this._size + 2)而不再是EnsureCapacity(this._size + 1)了。为了确保List的线程安全,我们必须保证在任意时刻只能有一个线程来更改List的数据。这样我们的SimpleThreadSafeList就诞生了,在这个List中我们对其的每一个读写操作都加上一个排它锁。

复制代码
public   class  SimpleThreadSafeList < T >  : IList < T >
{
    
private  List < T >  _data;
    
private   object  _syncRoot  =   new   object ();
    
public  SimpleThreadSafeList()
    {
        _data 
=   new  List < T > ();
    }
    
public   void  RemoveAt( int  index)
    {
        
lock  (_syncRoot)
        {
            _data.RemoveAt(index);
        }
    }
    
public   void  Add(T item)
    {
        
lock  (_syncRoot)
        {
            _data.Add(item);
        }
    }
    
//  others......
}
复制代码

这样我们确保List的数据在任意时刻都只有一个线程对其进行访问,看起来安全很多了。不过,如果所谓线程安全是这么简单的话,微软为什么不提供一个ThreadSafeList呢?JaredPar MSFT在这篇文章中做了一个描述《Why are thread safe collections so hard?http://blogs.msdn.com/b/jaredpar/archive/2009/02/11/why-are-thread-safe-collections-so-hard.aspx》,他解释微软不实现线程安全集合类,是因为以上这样的所谓“线程安全”并不是真正安全,我们可以从以下的代码中看出端倪。

var list  =   new  SimpleThreadSafeList < int > ();
//  ohters …
if (list.Count  >   0 )
{
    
return  list[ 0 ];
}

在以上代码中,我们创建了SimpleThreadSafeList这个类,其中有一个代码段就是用于获取list的默认值。如果有多个线程访问这段代码时,依然会出现数据不一致问题。即在执行“return list[0]”这条语句是,它是以“list.Count > 0”为前提的,当两个线程同时对这个SimpleThreadSafeList操作时,当前线程访问到list.Count大于0,但之后可能另一个线程将list清空了,这时候当前线程再来返回list[0]时就会出现IndexOutofRangeException了。SimpleThreadSafeList保证了List内部数据只能由一个线程来操作,但是对于上面的代码,它是无法保证数据不一致的。SimpleThreadSafeList仅能够被称为“数据线程安全”,这也是微软不提供线程安全集合类的原因了。JaredPar MSFT提出了一个真正解决线程安全的方法。那就是将SimpleThreadSafeList的_syncRoot 暴露出来。

复制代码
public   class  SimpleThreadSafeList < T >  : IList < T >
{
    
private   object  _syncRoot  =   new   object ();
    
public   object  SyncRoot
    {
        
get
        {
            
return  _syncRoot;
        }
    }
    
//  others……
}
复制代码

使用List时,需要使用到SyncRoot来加锁。 

复制代码
lock (list.SyncRoot)
{
    
if (list.Count  >   0 )
    {
        
return  list[ 0 ];
    }
}
复制代码

不过,使用这种方式,有几个缺陷。第一,没有一个良好的Guide来指导编写线程安全的代码;第二,当SyncRoot使用范围过大时,非常容易造成死锁。下面是一段可能产生死锁的代码。 

复制代码
var list1  =   new  SimpleThreadSafeList < int > ();
var list2 
=   new  SimpleThreadSafeList < int > ();

new  Thread(() =>  
    {
       
lock (list1.SyncRoot)
       {
           list2.Add(
10 );  // 间接请求了list2.SyncRoot
       } 
    }).Start();

new  Thread(() =>  
    {
       
lock (list2.SyncRoot)
       {
           list1.Add(
10 );  // 间接请求了list1.SyncRoot
       } 
    }).Start();
复制代码

对于死锁这个问题,我们采取的方法是使用Monitor.TryEnter,从而来避免一直死锁。对于前一个问题,我这边仅是基于DisposableLocker来实现尽可能的线程安全,对于如何使用,我目前依然没有一个良好的理论,只能说我们在设计高并发的API时,对多个线程可以同时访问的对象都需要加以判断从而来确定需要采用什么样的方式处理。

复制代码
namespace  UIShell.OSGi.Collection
{
    
///   <summary>
    
///  Use Monitor.TryEnter to acquire a lock. This would not cause the dead lock.
    
///   </summary>
     public   class  DisposableLocker : IDisposable
    {
        
private   object  _syncRoot;
        
private   bool  _lockAcquired;
        
private   int  _millisecondsTimeout;
        
public  DisposableLocker( object  syncRoot,  int  millisecondsTimeout)
        {
            _syncRoot 
=  syncRoot;
            _millisecondsTimeout 
=  millisecondsTimeout;
            _lockAcquired 
=  Monitor.TryEnter(_syncRoot, _millisecondsTimeout);
            LogWhenAcquireLockFailed();
        }
        
private   void  LogWhenAcquireLockFailed()
        {
            
if  ( ! _lockAcquired)   // 这时候可能要如下处理:(1)记录日志;(2)记录日志并抛出异常;(3)记录日志,然后Block,重现死锁。
            {
                FileLogUtility.Error(
string .Format(
                    
" Accquire the lock timeout. The limited time is {0} milliseconds. "
                    _millisecondsTimeout
                    ));
            }
        }
        
public   void  Dispose()
        {
            Dispose(
true );
            GC.SuppressFinalize(
this );
        }
        
private   void  Dispose( bool  disposing)
        {
            
if  (disposing)
            {
                
if  (_lockAcquired)
                {
                    Monitor.Exit(_syncRoot);
                }
            }
        }
        
~ DisposableLocker()
        {
            Dispose(
false );
        }
    }
}
复制代码

当出现死锁时,这里采用的解决方案是记录日志,然后继续运行。不过,这种方法有一定缺陷,可能在极端情况下引起数据不一致。因此,我们可能需要抛出异常或者让锁一直持续下去。SimpleThreadSafeList此时将以以下方式来实现。

复制代码
public   class  SimpleThreadSafeList < T >  : IList < T >
{
    private   object  _syncRoot  =   new   object ();
    public   object  SyncRoot
    {
        get
        {
            return  _syncRoot;
        }
    }
    public   int  MillisecondsTimeoutOnLock {  get private   set ; }
    public  DisposableLocker CreateLocker()
    {
        return   new  DisposableLocker(MillisecondsTimeoutOnLock, SyncRoot);
    }
    public   void  Add(T item)
    {
        using (CreateLocker())
        {
            _data.Add(item);
        }
    }
    //  others……
}
复制代码

接下来使用List.CreateLocker来建立一个全局锁

复制代码
using (list.CreateLocker())
{
    if (list.Count  >   0 )
    {
        return  list[ 0 ];
    }
}
复制代码

大家可以看一下是否还有更好的方式来保证线程绝对安全。


本文转自道法自然博客园博客,原文链接:http://www.cnblogs.com/baihmpgy/archive/2011/07/13/2105018.html,如需转载请自行联系原作者

相关实践学习
【涂鸦即艺术】基于云应用开发平台CAP部署AI实时生图绘板
【涂鸦即艺术】基于云应用开发平台CAP部署AI实时生图绘板
目录
相关文章
|
数据采集 Java API
Jsoup库能处理多线程下载吗?
Jsoup库能处理多线程下载吗?
|
10月前
|
负载均衡 算法 安全
基于Reactor模式的高性能网络库之线程池组件设计篇
EventLoopThreadPool 是 Reactor 模式中实现“一个主线程 + 多个工作线程”的关键组件,用于高效管理多个 EventLoop 并在多核 CPU 上分担高并发 I/O 压力。通过封装 Thread 类和 EventLoopThread,实现线程创建、管理和事件循环的调度,形成线程池结构。每个 EventLoopThread 管理一个子线程与对应的 EventLoop(subloop),主线程(base loop)通过负载均衡算法将任务派发至各 subloop,从而提升系统性能与并发处理能力。
527 3
|
10月前
|
安全 算法 Java
Java 多线程:线程安全与同步控制的深度解析
本文介绍了 Java 多线程开发的关键技术,涵盖线程的创建与启动、线程安全问题及其解决方案,包括 synchronized 关键字、原子类和线程间通信机制。通过示例代码讲解了多线程编程中的常见问题与优化方法,帮助开发者提升程序性能与稳定性。
423 0
|
7月前
|
JSON 网络协议 安全
【Java】(10)进程与线程的关系、Tread类;讲解基本线程安全、网络编程内容;JSON序列化与反序列化
几乎所有的操作系统都支持进程的概念,进程是处于运行过程中的程序,并且具有一定的独立功能,进程是系统进行资源分配和调度的一个独立单位一般而言,进程包含如下三个特征。独立性动态性并发性。
395 1
|
7月前
|
JSON 网络协议 安全
【Java基础】(1)进程与线程的关系、Tread类;讲解基本线程安全、网络编程内容;JSON序列化与反序列化
几乎所有的操作系统都支持进程的概念,进程是处于运行过程中的程序,并且具有一定的独立功能,进程是系统进行资源分配和调度的一个独立单位一般而言,进程包含如下三个特征。独立性动态性并发性。
366 1
|
Linux 程序员 API
CentOS如何使用Pthread线程库
这就是在CentOS下使用Pthread线程库的全过程。可见,即使是复杂的并发编程,只要掌握了基本的知识与工具,就能够游刃有余。让我们积极拥抱并发编程的魅力,编写出高效且健壮的代码吧!
277 11
|
监控 安全 Java
Java多线程调试技巧:如何定位和解决线程安全问题
Java多线程调试技巧:如何定位和解决线程安全问题
376 2
|
安全 算法 Java
【Java集合类面试二】、 Java中的容器,线程安全和线程不安全的分别有哪些?
这篇文章讨论了Java集合类的线程安全性,列举了线程不安全的集合类(如HashSet、ArrayList、HashMap)和线程安全的集合类(如Vector、Hashtable),同时介绍了Java 5之后提供的java.util.concurrent包中的高效并发集合类,如ConcurrentHashMap和CopyOnWriteArrayList。
【Java集合类面试二】、 Java中的容器,线程安全和线程不安全的分别有哪些?
|
安全 Java 程序员
【多线程-从零开始-肆】线程安全、加锁和死锁
【多线程-从零开始-肆】线程安全、加锁和死锁
320 0
|
安全 Java
LinkedBlockingQueue 是线程安全的,为什么会有两个线程都take()到同一个对象了?
LinkedBlockingQueue 是线程安全的,为什么会有两个线程都take()到同一个对象了?
489 0

热门文章

最新文章