不变对象是指对象的状态在构造后不可改变。这从根本上消除了线程间同步的需求,与锁或者阻塞策略不同的是,不变对象对运行时和设计时不会带来任何额外的开销,因此不变对象是多线程编程中一个很基本的策略。
最简单的不变对象是没有任何状态变量(静态或实例变量)的对象,但在实际编程中出现得更多的是构造后状态不可变的对象,下面是一个简单的例子:
一个不变对象
1 public class ImmutablePoint
2 {
3 private readonly int _x;
4 private readonly int _y;
5
6 public ImmutablePoint(int x, int y)
7 {
8 _x = x;
9 _y = y;
10 }
11
12 public int X
13 {
14 get { return _x; }
15 }
16
17 public int Y
18 {
19 get { return _y; }
20 }
21 }
22
对习惯了普通编程语言的程序员来说,很少编写不变对象,但是有一些策略可以让我们开始使用它:
对数据容器使用不变对象
对某些数据容器,比如应用程序的配置字典,可能应用程序一旦运行就不会改变,不变对象是个很合适的选择
一个不变的配置类
1 public class Configuration
2 {
3 private readonly Dictionary<string, string> _configs;
4
5 public Configuration()
6 {
7 _configs = new Dictionary<string, string>();
8 ReadConfig();
9 }
10
11 public string this[string key]
12 {
13 get { return _configs[key]; }
14 }
15
16 private void ReadConfig()
17 {
18 //从配置文件或数据库中读取配置值
19 }
20 }
21
Copy-on-Write技术
看了上面的例子,有朋友可能会说:我们的应用程序配置是可以运行时更新的,那还能使用不变对象吗?答案是:用点小技巧,我们仍然可以享受不变对象的优点。
先看用锁的方法,因为配置的字典可能会变化,因此使用了一个读写锁对象,比起使用Lock,读写锁的优势在于读操作可以并行,只有在更新时才存在独占的情况:
使用锁来保证配置读写的安全
1 public string this[string key]
2 {
3 get
4 {
5 readWriteLockObject.Read();
6 try
7 {
8 return _configs[key];
9 }
10 finally
11 {
12 readWriteLockObject.ReleaseReadLock();
13 }
14 }
15 }
16
17 public void ReadConfig()
18 {
19 readWriteLockObject.Write();
20 try
21 {
22 //从配置文件或数据库中读取配置值
23 }
24 finally
25 {
26 readWriteLockObject.ReleaseWriteLock();
27 }
28 }
29
上面的方法有个缺点:当更新配置时,所以的读操作都必须等待,如果更新的时间比较长,在IIS中就意味着大量的页面请求被阻塞甚至可能超时。下面看看使用不变对象的例子:
使用内部不变对象减少锁的开销
1 public class Configuration1
2 {
3 private ImmutableConfiguration _config;
4 private RWLockObject _readWriteLockObject;
5
6 public Configuration1()
7 {
8 _config = new ImmutableConfiguration();
9 }
10
11 public string this[string key]
12 {
13 get
14 {
15 ImmutableConfiguration config;
16 _readWriteLockObject.Read();
17 try
18 {
19 config = _config;
20 }
21 finally
22 {
23 _readWriteLockObject.ReleaseReadLock();
24 }
25 return config[key];
26 }
27 }
28
29 public void ReadConfig()
30 {
31 ImmutableConfiguration config = new ImmutableConfiguration();
32 _readWriteLockObject.Write();
33 try
34 {
35 _config = config;
36 }
37 finally
38 {
39 _readWriteLockObject.ReleaseWriteLock();
40 }
41 }
42
43 class ImmutableConfiguration
44 {
45 private readonly Dictionary<string, string> _configs;
46
47 public ImmutableConfiguration()
48 {
49 _configs = new Dictionary<string, string>();
50 ReadConfig();
51 }
52
53 public string this[string key]
54 {
55 get { return _configs[key]; }
56 }
57
58 private void ReadConfig()
59 {
60 //从配置文件或数据库中读取配置值
61 }
62 }
63 }
64
在上面的代码中, 更新配置时锁定的时间很短,从而减轻了读线程被阻塞的可能,更进一步,考虑到读写锁都只包含单个赋值语句,还可以考虑使用volatile read/write来完全取消对读写锁的要求。
使用Copy-on-Write方法的另一个例子是某些集合对象,对某些很少改变的集合对象,我们也可以建立一个存储了集合数据的内部不可变对象,当需要遍历集合时,返回此对象,而需要改变集合对象时,我们可以复制原来的数据建立起新的不可变对象进行替换,代码和上面的非常类似,这里就不写出来了。在合适的场景下,这种方法可以大大减少线程同步对并行性能的影响。
如果对象的一组成员变量具有内在的联系或者约束,那么我们可以将这些变量放到一个新的不变对象中,使用Copy-on-Write,可以减少同步一组变量的开销。
例如,一个人的地址是由国家、省(州)、城市等变量描述的,下面是对此问题的两种代码:
使用多个成员变量的代码:
一个普通的Person类
1 public class Person
2 {
3 private string _country;
4 private string _state;
5 private string _city;
6 private string _name;
7 private object _lock = new object();
8
9 public string Country
10 {
11 get { lock(_lock) return _country; }
12 }
13 //State、City、Name等其他属性的代码。。。
14
15 public void ChangeAddress(string country, string state, string city)
16 {
17 lock (_lock)
18 {
19 _country = country;
20 _state = state;
21 _city = city;
22 }
23 }
24 }
25
使用不变对象的代码:
使用不变对象的Person类
1 public class Person
2 {
3 private Address _address;
4 private string _name;
5 private object _lock = new object();
6
7 public string Country
8 {
9 get
10 {
11 Address address = Thread.VolatileRead(ref _address);
12 return address.Country;
13 }
14 }
15 //Name等其他属性的代码。。。
16
17 public void ChangeAddress(string country, string state, string city)
18 {
19 Address address = new Address(country, state, city);
20 Thread.VolatileWrite(ref _address, address);
21 }
22
23 //新的不变对象类
24 class Address
25 {
26 private readonly string _country;
27 private readonly string _state;
28 private readonly string _city;
29
30 public Address(string country, string state, string city)
31 {
32 _country = country;
33 _state = state;
34 _city = city;
35 }
36
37 public string Country { get { return _country; } }
38 public string State { get { return _state; } }
39 public string City { get { return _city; } }
40 }
41 }
42
从上面代码的对比可以看到,使用不变对象减少了对多个读写操作协调的需要,当多个变量被一个不变对象取代后,我们就可以使用轻量级的volatile read/write来完全消除锁的使用。
为普通对象加上只读适配器
在有些时候,我们需要为一个普通的可读写对象返回一个只读的版本,这时候如果我们能建立一个只读的适配器,就无需在外部代码中加入同步代码。
例如我们在编写帐务管理系统时可能有一个Account对象,可以读取账户的余额或者修改其余额,但很多时候我们只需要查询账户,这时就可以提供一个不变版本的Account对象:
增加了只读适配器的Account对象
1 public interface IImmutableAccount
2 {
3 int Balance { get; }
4 }
5
6 public interface IAccount : IImmutableAccount
7 {
8 void Credit(int credit);
9 }
10
11 public class Account : IAccount
12 {
13 private int _balance;
14 private RWLockObject _readWriteLockObject;
15
16 public Account(int balance)
17 {
18 _balance = balance;
19 }
20
21 public int Balance
22 {
23 get
24 {
25 _readWriteLockObject.Read();
26 try
27 {
28 return _balance;
29 }
30 finally
31 {
32 _readWriteLockObject.ReleaseReadLock();
33 }
34 }
35 }
36
37 public void Credit(int amount)
38 {
39 _readWriteLockObject.Write();
40 try
41 {
42 _balance = _balance + amount;
43 }
44 finally
45 {
46 _readWriteLockObject.ReleaseWriteLock();
47 }
48 }
49
50 public void Dedit(int amount)
51 {
52 return Credit(-1 * amount);
53 }
54 }
55
56 public class ImmutableAccount : IImmutableAccount
57 {
58 private readonly Account _account;
59 public ImmutableAccount(Account account)
60 {
61 _account = account;
62 }
63
64 public int Balance
65 {
66 get { return _account.Balance; }
67 }
68 }
69
Flyweight(享元)模式
Flyweight模式是GOF《设计模式》中的一个经典标准设计模式,里面的共享对象就是典型的不变对象,这方面的文章可以从Google上找到很多,这里就不班门弄斧了。
关于This逸出问题
我们在使用不可变对象时,要要格外注意下面的原则:
这一点不光是不可变对象,也是设计任何类都应该遵守的原则。通常会导致这个问题的因素是在构造函数中以This为参数调用非私有函数或其他外部函数,这使得其他线程有机会访问到未完全初始化的对象。
对不变模式的总结
不变对象由于其优点,值得在多线程编程中予以重视。但不变对象也有明显的缺点,就是大量对象导致的内存占用巨大的问题。因此在使用不变对象前,需要我们在设计上进行权衡,变化过于频繁的对象是不适合使用这种策略的。
待续。。。