关于“读写锁”:在锁住时,读的时候可以多线程并发,但是一旦开始写入,则其余的“写”以及其余的“读”都等待直到“写”结束方能访问此对象。
仅锁住“写”:“写”的时候锁住线程,此时其它“写”不能访问此对象,但是其它读可以访问此对象。若使用“读写锁”,则“写”的时候,“读”线程也不能访问。
对于Dictionary 类的同步,通常都有两种方法,其一是仅锁住“写”,但是读不锁。
实现方法如下:
public void add(tkey key, tvalue value) { lock (syncroot) { d.add(key, value); } }其二是用“读写锁”,这篇文章写得很详细,通时对性能进行了评测:
http://www.cnblogs.com/JeffreyZhao/archive/2009/11/12/concurrent-cache-performance-improvement-1-immutable-hash-table.html
在此文章中提到仅仅锁住“写”是不行的,对此我不敢苟同。
第一:hash表的读的方法是以下几步:①获得表的基本信息,②根据表的基本信息计算key的hash值,就是将byte的移位,速度非常快,③根据hash值在数组中查找数据。这几步任意一步都不影响表结构本身,也就是说“读”不对“写”造成影响,也就是说在“写”的同时进行“读”是不会破坏链表的。但是“写”会对“读”造成影响,因为“写”本身可能会使内存扩容,可能会遇到碰撞而重新hash等因素,使“读”的第一步获得的表的基本信息是过时的,然后第二步第三步就出问题。但是这种几率非常小,为什么这么说呢,因为hash表中的读,速度非常快,就是几步byte移位以及一个定位操作,经过测试,加“读写锁”后,在单线程中光是“锁”的操作时间就是“读”的操作时间的两倍,在多个线程中切换时“锁”的操作时间可能数倍于“读”的操作时间。实际上测试读1千万次都只需要几百毫秒,而“读”出错的时候仅限于“写”时恰好扩容或者“写”时悄好发生hash同值碰撞的那一瞬间,这么夸张的小几率事件,若对程序要求不是特别特别的高,不是银行系统,则是不需要考虑的。
官方对于dictionary的线程安全是这样描述的:
“只要不修改该集合,Dictionary 就可以同时支持多个阅读器。即便如此,从头到尾对一个集合进行枚举本质上并不是一个线程安全的过程。当出现枚举与写访问互相争用这种极少发生的情况时,必须在整个枚举过程中锁定集合。若要允许多个线程访问集合以进行读写操作,则必须实现自己的同步。”
那么分析可得,官方认为“写”是一定不安全的,光“读”是一定安全的。“读写”同时操作时,“枚举”和“写”同时操作时有“极少发生”的互相争用对象的几率。“枚举”相当于一次性连续读取全部hash表数据,若hash表有一万个数据,则需要枚举一万次,时间上和读取一次,相差万倍,就算这样,官方仍旧认为是小概率事件。
所以,最后我认为,在网络应用程序中,如web网页应用中,对于dictionary,仅仅“写”锁定,以及“枚举”锁定就可以,“读”是不需要锁定的。万一万一恰好恰好读的时候出错了怎么办?呵呵,那也很好办啊,再刷新一下页面就是了。只要写锁定了,链表就不会出错,对系统本身就不构成影响。比方说若一秒钟写入一次数据,写入数据耗时大约10倍于读取,就算1千万分之一秒写入,其中恰好扩容或者碰撞开始影响表结构的一瞬间算几亿分之一秒,然后读取的时间也是一亿分之一秒。这个相乘就是出错的几率,再呢,很多写入的时候并不扩容,也不碰撞,则出错的几率更低。出现读错的现象几率估计是几十亿 亿分之一吧。而且读错了对程序本身是没有影响的,仅对一个页面反应有影响。