谈谈多线程编程(二)- 不变对象

不变对象是指对象的状态在构造后不可改变。这从根本上消除了线程间同步的需求,与锁或者阻塞策略不同的是,不变对象对运行时和设计时不会带来任何额外的开销,因此不变对象是多线程编程中一个很基本的策略。

最简单的不变对象是没有任何状态变量(静态或实例变量)的对象,但在实际编程中出现得更多的是构造后状态不可变的对象,下面是一个简单的例子: 

一个不变对象

对习惯了普通编程语言的程序员来说,很少编写不变对象,但是有一些策略可以让我们开始使用它:

对数据容器使用不变对象

 对某些数据容器,比如应用程序的配置字典,可能应用程序一旦运行就不会改变,不变对象是个很合适的选择

一个不变的配置类

Copy-on-Write技术

 看了上面的例子,有朋友可能会说:我们的应用程序配置是可以运行时更新的,那还能使用不变对象吗?答案是:用点小技巧,我们仍然可以享受不变对象的优点。

先看用锁的方法,因为配置的字典可能会变化,因此使用了一个读写锁对象,比起使用Lock,读写锁的优势在于读操作可以并行,只有在更新时才存在独占的情况:

使用锁来保证配置读写的安全

上面的方法有个缺点:当更新配置时,所以的读操作都必须等待,如果更新的时间比较长,在IIS中就意味着大量的页面请求被阻塞甚至可能超时。下面看看使用不变对象的例子

使用内部不变对象减少锁的开销

在上面的代码中, 更新配置时锁定的时间很短,从而减轻了读线程被阻塞的可能,更进一步,考虑到读写锁都只包含单个赋值语句,还可以考虑使用volatile read/write来完全取消对读写锁的要求。

使用Copy-on-Write方法的另一个例子是某些集合对象,对某些很少改变的集合对象,我们也可以建立一个存储了集合数据的内部不可变对象,当需要遍历集合时,返回此对象,而需要改变集合对象时,我们可以复制原来的数据建立起新的不可变对象进行替换,代码和上面的非常类似,这里就不写出来了。在合适的场景下,这种方法可以大大减少线程同步对并行性能的影响。

如果对象的一组成员变量具有内在的联系或者约束,那么我们可以将这些变量放到一个新的不变对象中,使用Copy-on-Write,可以减少同步一组变量的开销。
例如,一个人的地址是由国家、省(州)、城市等变量描述的,下面是对此问题的两种代码:

使用多个成员变量的代码:

一个普通的Person类

 

使用不变对象的代码:

使用不变对象的Person类

 

从上面代码的对比可以看到,使用不变对象减少了对多个读写操作协调的需要,当多个变量被一个不变对象取代后,我们就可以使用轻量级的volatile read/write来完全消除锁的使用。

为普通对象加上只读适配器

 在有些时候,我们需要为一个普通的可读写对象返回一个只读的版本,这时候如果我们能建立一个只读的适配器,就无需在外部代码中加入同步代码。

例如我们在编写帐务管理系统时可能有一个Account对象,可以读取账户的余额或者修改其余额,但很多时候我们只需要查询账户,这时就可以提供一个不变版本的Account对象:

增加了只读适配器的Account对象

 

Flyweight(享元)模式

 Flyweight模式是GOF《设计模式》中的一个经典标准设计模式,里面的共享对象就是典型的不变对象,这方面的文章可以从Google上找到很多,这里就不班门弄斧了

关于This逸出问题

我们在使用不可变对象时,要要格外注意下面的原则:

  • 在构造函数执行前,不要访问对象的数据

这一点不光是不可变对象,也是设计任何类都应该遵守的原则。通常会导致这个问题的因素是在构造函数中以This为参数调用非私有函数或其他外部函数,这使得其他线程有机会访问到未完全初始化的对象。

对不变模式的总结

不变对象由于其优点,值得在多线程编程中予以重视。但不变对象也有明显的缺点,就是大量对象导致的内存占用巨大的问题。因此在使用不变对象前,需要我们在设计上进行权衡,变化过于频繁的对象是不适合使用这种策略的。

                                                                                                                            待续。。。

posted @ 2008-12-02 10:13  在路上的牛  阅读(2074)  评论(3编辑  收藏  举报