2 {
3 public void DoMultiThreadedWork(object someParameter)
4 {
5 lock (lockObject)
6 {
7 // lots of work
8 }
9 }
10
11 private string lockObject = "lockit";
12 }
这段代码很简单,大家看看这段代码有什么问题呢?
开始这么一看似乎没什么大的问题。可是仔细分析一下代码你就可以知道其中还是有一些很严重的问题的。假如你对Lock机制有所了解并且对string类型有过研究的话你就会发现出问题:
lock 关键字的参数必须为基于引用类型的对象,该对象用来定义锁的范围。在上例中,锁的范围限定为此函数,因为函数外不存在任何对该对象的引用。严格地说,提供给 lock 的对象只是用来唯一地标识由多个线程共享的资源,所以它可以是任意类实例。然而,实际上,此对象通常表示需要进行线程同步的资源。例如,如果一个容器对象将被多个线程使用,则可以将该容器传递给 lock,而 lock 后面的同步代码块将访问该容器。只要其他线程在访问该容器前先锁定该容器,则对该对象的访问将是安全同步的。通常,最好避免锁定 public 类型或锁定不受应用程序控制的对象实例。因为不受控制的代码也可能会锁定该对象。这可能导致死锁,即两个或更多个线程等待释放同一对象。关于Lock被编译器编译过后的代码请参考我的另一篇文章:Inside C#
String在CLR中有两个重要的属性:不变性和字符串驻留。这意味着整个程序中任何给定字符串都只有一个实例,就是这同一个对象表示了所有运行的应用程序域的所有线程中的该文本。因此,只要在应用程序进程中的任何位置处具有相同内容的字符串上放置了锁,就将锁定应用程序中该字符串的所有实例。因此,最好锁定不会被暂留的私有或受保护成员。
所以锁定字符串尤其危险。
下面我们来看看字符串驻留是个怎样的东东。看看下面的代码
2 string b = "lockit";
3 string c = "LOCKIT".ToLower();
4 string d = "lock" + "it";
5 string e1 = "lock";
6 string e2 = "it";
7 string ee = e1 + "it";
8 string e = e1 + e2;
9 Console.WriteLine(object.ReferenceEquals(a, b));
10 Console.WriteLine(object.ReferenceEquals(a, c));
11 Console.WriteLine(object.ReferenceEquals(a, d));
12 Console.WriteLine(object.ReferenceEquals(a, ee));
13 Console.WriteLine(object.ReferenceEquals(a, e));
下边是输出的结果:
True,False,True,False,False
我们知道CLR处理每个对象在内存的时候都会额外的生成一个syncblockindex空间,这个就是用于对象同步用的。是大家平时开发使用最多的一个类型,MS的CLR部门为了简化操作和性能的优化做了两点处理,一是将string的创建过程简单化。一般的对象在创建的时候通过new关键字来实现;而string不需要这么做,我们只需要把对应的字符换赋给给对应的字符串变量就可以了。那么他们在创建过程中使用的MSIL指令时不同的——一般的引用对象的创建是通过newobj这样一个IL指令来实现的,而创建一个字符串变量的IL指令则是ldstr(load string)。其次就是为了考虑性能的提升和内存节约上,对于创建相同的字符串,一般不会为他们分别分配内存块,相反地,他们会共享一块内存。CLR内部维护一个HashTable,这HashTable维护者大部分创建的string。这个HashTable的Key对应的相应的string本身,而Value则是分配给这个string的内存块的引用。我们知道在一个托管进程被创建以后,在托管进程的内存空间里面,包含了System Domain,Shared Domain等等,而这个HashTable就是放在System Domain里,所以它是在这整个程序的生命周期里都是存在的和被共享的。一般地,在程序运行过程中,如果需要的创建一个string,CLR会根据这个string的Hash Code试着在HashTable中找这个相同的string,如果找到,则直接把找到的string的地址赋给相应的变量,如果没有则在托管堆中创建一个string,CLR会先在managed heap中创建该strng,并在HashTable中创建一个Key-Value,Key为这个string本身,Value为string的内存地址,这个地址最重被赋给响应的变量。
上面的例子后面两个为False告诉我们对于对一个动态创建的字符串,驻留机制便不会起作用。这种情况产生的MSIL也不一样。但是我们可以用System.String中的静态方法Intern来解决。
公共语言运行库通过维护一个表来存放字符串,该表称为拘留池,它包含程序中以编程方式声明或创建的每个唯一的字符串的一个引用。因此,具有特定值的字符串的实例在系统中只有一个。
例如,如果将同一字符串分配给几个变量,运行库就会从拘留池中检索对该字符串的相同引用,并将它分配给各个变量。
Intern 方法使用拘留池来搜索与 str 值相等的字符串。如果存在这样的字符串,则返回拘留池中它的引用。如果不存在,则向拘留池添加对 str 的引用,然后返回该引用。
在下面的 C# 示例中,值为“MyTest”的字符串 s1 已经留用,因为它在程序中是一个字符串常量。
System.Text.StringBuilder 类生成与 s1 同值的新字符串对象。对该字符串的引用被分配给 s2。
Intern 方法搜索与 s2 具有相同值的字符串。由于存在这样的字符串,该方法会返回分配给 s1 的同一引用,然后将该引用分配给 s3。
引用 s1 和 s2 的比较结果是不相等的,这是因为它们引用的是不同的对象;而引用 s1 和 s3 的比较结果是相等的,因为它们引用的是相同的字符串。
String s1 = "MyTest"; String s2 = new StringBuilder().Append("My").Append("Test").ToString(); String s3 = String.Intern(s2); Console.WriteLine((Object)s2==(Object)s1); // Different references. Console.WriteLine((Object)s3==(Object)s1); // The same reference.
将此方法与 IsInterned 方法进行比较。