我们知道,在.net的一些集合类型中,譬如Hashtable和ArrayList,都有Synchronized静态方法和SyncRoot属性,他们之间有联系吗?我怎么才能用好他们呢? 以Hashtable为例,看看他们的基本用法:
Synchronized表示返回一个线程安全的Hashtable,什么样的 hashtable才是一个线程安全的呢?下边我们就从.NET的源码开始理解。
从源码不难看出,Synchronized方法返回的其实是一个SynchHashtable类型的实例。在前边我们说过,Synchronized表示返回一个线程安全的Hashtable,从这个解释不难看出,SynchHashtable应该是继承自Hashtable。下边我们验证一下。看看 SynchHashtable类型的源码:
呵呵,果然不出我们所料,SyncHashtable果然继承自Hashtable,SyncHashtable之所有能实现线程的安全操作,就是因为在他们的一些方法中,就加了lock,我们知道,哪一个线程执行了lock操作,在他还没有释放lock之前,其他线程都要处于堵塞状态。 SyncHashtable就是通过这种方法,来实现所谓的线程安全。
现在我们理解了Synchronized的含义和用法,那接下来我们看看他和SyncRoot之间的关系。 SyncRoot表示获取可用于同步 Hashtable 访问的对象,老实说,这个解释不好理解,要想真正理解他的用法,我们还得从源码开始:
如果您清楚Interlocked的用法,这段代码没什么难理解的了(不清楚的朋友找GOOGLE吧),Interlocked为多个线程共享的变量提供原子操作。原子操作就是单线程操作。在一个Hashtable实例中,不论我们在代码的任何位置调用SyncRoot,返回的都是同一个object类型的对象。我们在开始写的lock(ht.SyncRoot)和下边的操作作用是一样的:
他们之间不同的是,我们声明的static object类型对象是类型级别的,而SyncRoot是对象级别的。
通过上面的分析,我们都应该能理解Synchronized 和 SyncRoot用法,他们之间的关系就是: Hashtable 通过Synchronized方法,生成一个SynchHashtable类型的对象,在这个对象的一个方法中,通过lock (this._table.SyncRoot)这样的代码来实现线程安全的操作,其中this._table.SyncRoot返回的就是一个 object类型的对象,在一个SynchHashtable对象实例中,不管我们调用多少次,他是唯一的。
在许多的集合类中,都能看到Syncronized静态方法和SyncRoot实例属性,这两个单词的sync就显而易见的说明了是用来实现同步的。集合类通常不是线程安全的,多个读取器可以安全的读取集合.但是对集合的任何修改都将为访问集合的所有线程生成不明确的结果,对于集合类来讲,我们可以使用下面两种方式来避免:
(1) Synchronized 方法,并通过该包装以独占方式访问集合,编译器会自动生成适当的 Monitor.Enter 和 Monitor.Exit 调用
(2) 在访问该集合时对SyncRoot属性使用锁定机制
这两种方式到底有哪些区别的,实际应用时应该使用哪种方法呢?
首先需要明确的是Synchronized 并不能保证枚举的同步,如果没有任何线程在读取 Hashtable,则 Synchronized 支持使用多个写入线程。如果使用一个(或多个)读取器以及一个(或多个)编写器,则同步包装不提供线程安全的访问,也就是说使用这个方法并不能保证枚举的同步,例如,一个线程正在删除或添加集合项,而另一个线程同时进行枚举,这时枚举将会抛出异常。所以,在枚举的时候,你必须明确锁定这个集合。这时就要用到SyncRoot。
什么是SyncRoot呢,可以这样认为,一些集合类自己维护着一个内部的数据结构,而SyncRoot就是这个一个内部对象,如果给了对这个内部数据结构的访问权,那么仅仅锁定集合对象是无用的。此时就要用到SyncLock(ht.SyncRoot)来保证集合的变化。
今天同事告诉我, 锁 hashtable 应该锁它的 SyncRoot 属性而不应该锁它的实例, 例如:
Hashtable ht = new Hashtable();
lock(ht.SyncRoot){ ...}看了 .Net Framework 文档, 给的例子也是锁 SyncRoot 属性, 说如果锁实例的话不能保证在并发情况下的同步, 我很疑惑, 为什么不能锁 hashtable 实例本身呢?
做了个实验, 两个线程 A 和 B, 用锁实例和锁 SyncRoot 两种方式测试, 都没有问题, 结果是一样的。
后来, 用 Hashtable.Synchronized 创建自动线程同步的 hashtable, 终于明白了 SyncRoot 的作用。先说说自动线程同步的 Hashtable: 如果 Hashtable 要允许并发读但只能一个线程写, 要这么创建 Hashtable 实例:
Hashtable hashtable = Hashtable.Synchronized(new Hashtable());
这样, 如果有多个线程并发的企图写 hashtable 里面的 item, 则同一时刻只能有一个线程写, 其余阻塞; 对读的线程则不受影响。
测试的代码是这样的:
Hashtable _hashtable = Hashtable.Synchronized(new Hashtable());
public void TestLock()
{ Thread t1 = new Thread(new ThreadStart(SyncFunctionA));Thread t2 = new Thread(new ThreadStart(SyncFunctionB));t1.Start();
t2.Start();Thread.Sleep(8000);
Console.WriteLine("hashtable[" + _key_a + "] = " + _hashtable[_key_a]);
}private void SyncFunctionA()
{ lock (_hashtable.SyncRoot){ Thread.Sleep(5000);_hashtable[_key_a] = "Value set by SyncFunctionA";}}private void SyncFunctionB()
{ Console.WriteLine("hashtable[" + _key_a + "] = " + _hashtable[_key_a]);_hashtable[_key_a] = "Value set by SyncFunctionB";}为了清楚的看到效果, 线程 A 用了锁, 并睡眠 5 秒, 睡醒后设置一下 hashtable 里的 item. 线程 B 先读一下 hashtable 里的 item, 再写 hashtable 里的 item。因为对 SyncRoot 加了锁, 即使线程 B 没有显式的对 hashtable 加锁, 但在 _hashtable[_key_a] = "Value set by SyncFunctionB" 一句上也会被 hashtable 自动锁住, 直到线程 A 释放掉 SyncRoot 锁为止。如果线程 A 不是锁 SyncRoot 而是锁 hashtable 实例本身, 那么线程 B 不会在 _hashtable[_key_a] = "Value set by SyncFunctionB" 上被自动锁住。
所以, 总结如下:
如果想锁整个 hashtable, 包括读和写, 即不允许并发的读和写, 那应该锁 hashtable 实例;
如果想允许并发的读, 不允许并发的写, 那应该创建 Synchronized 的 hashtable, 并对要加锁的一块代码用 SyncRoot 锁住, 如果不需要对一块代码加锁, 则 hashtable 会自动对单个写的操作加锁。
另外,针对泛型集合的线程安全访问,由于泛型集合中没有直接公布SyncRoot属性,所以猛一看好似无从下手。
但是查看集合泛型集合的源代码后就可发现他们实际上都提供了SyncRoot属性。
以下以Queue<T>集合为例。
从以上源代码可以看出,这两个方法都被实现为了显式接口,所以必须将其显式转型为ICollection后才能使用。
总结如下
1、在非泛型结构中,Synchronized和SyncRoot可以配合使用,其实Synchronized内部用的也是SyncRoot实现同步访问。但是Synchronized的读并没有加锁,所以同时读写还是会出问题。
2、Synchronized和SyncRoot只能实现对写的同步,如果要实现读和写的同步,还要自己使用lock(this)进行同步。
3、泛型数据类只能使用SyncRoot,所以同步还是要自己实现。
System.Collection.Generic空间下的类都不是线程安全类,比如要使用一个线程同步的队列,还要手动对Queue<T>进行同步操作。
而使用System.Collection.Concurrent 空间下的类可以实现线程同步。
但是这些同步的数据结构并不是阻塞的,不管是否有数据可取,取得方法都是马上返回,根据返回的bool值决定是否取成功。这样就要有一个轮询来不断检查是否有新数据可以取,或者自己设计内核对象来通知有新的数据可以取。
这样的话还是要对该数据结构进行封装。