大家都知道引用类型对象除实例字段的开销外,还有两个字段的开销:类型指针和同步块索引(SyncBlockIndex)。同步块索引这个东西比起它的兄弟类型指针更少受人关注,显得有点冷落,其实此兄功力非凡,在CLR里可谓叱咤风云,很多功能都要借助它来实现。 接下来我会用三篇来介绍同步块索引在.NET中的所作所为。
既然本章副标题是从lock开始,那我就举几个lock的示例:
代码1
public class Singleton 2: { 3: private static object lockHelper = new object(); 4: private static Singleton _instance = null; 5: public static Singleton Instance 6: { 7: get 8: { 9: lock(lockHelper) 10: { 11: if(_instance == null) 12: _instance = new Singleton(); 13: } 14: return _instance; 15: } 16: } 17: }
代码2
public class Singleton 2: { 3: private static Singleton _instance = null; 4: public static Singleton Instance 5: { 6: get 7: { 8: object lockHelper = new object(); 9: lock(lockHelper) 10: { 11: if(_instance==null) 12: _instance = new Singleton(); 13: } 14: return _instance; 15: } 16: } 17: }
代码3
public class Singleton 2: { 3: private static Singleton _instance = null; 4: public static Singleton Instance 5: { 6: get 7: { 8: lock(typeof(Singleton)) 9: { 10: if(_instance==null) 11: _instance = new Singleton(); 12: } 13: return_instance; 14: } 15: } 16: }
代码4
public void DoSomething() 2: { 3: lock(this) 4: { 5: //dosomething 6: } 7: }
上面四种代码,对于加锁的方式来说(不讨论其他)哪一种是上上选?对于这个问题的答案留在本文最后解答。
让我们先来看看在Win32的时代,我们如何做到CLR中的lock的效果。在Win32时,Windows为我们提供了一个CRITICAL_SECTION结构,看看上面的单件模式,如果使用CRITICAL_SECTION的方式如何实现?
1: class Singleton 2: { 3: private: 4: CRITICAL_SECTIONg_cs; 5: static Singleton _instance = NULL; 6: public: 7: Singleton() 8: { 9: InitializeCriticalSection(&g_cs); 10: } 11: static Singleton GetInstance() 12: { 13: EnterCriticalSection(&g_cs); 14: if(_instance!=NULL) 15: _instance=newSingleton(); 16: LeaveCriticalSection(&g_cs); 17: return_instance; 18: } 19: ~Singleton() 20: { 21: DeleteCriticalSection(&g_cs); 22: } 23: }
Windows提供四个方法来操作这个CRITICAL_SECTION,在构造函数里我们使用InitializeCriticalSection这个方法初始化这个结构,它知道如何初始化CRITICAL_SECTION结构的成员,当我们要进入一个临界区访问共享资源时,我们使用EnterCriticalSection方法,该方法首先会检查CRITICAL_SECTION的成员,检查是否已经有线程进入了临界区,如果有,则线程会等待,否则会设置CRITICAL_SECTION的成员,标识出本线程进入了临界区。当临界区操作结束后,我们使用LeaveCriticalSection方法标识线程离开临界区。在Singleton类的析构函数里,我们使用DeleteCriticalSection方法销毁这个结构。整个过程就是如此。
我们可以在WinBase.h里找到CRITICAL_SECTION的定义:
typedef RTL_CRITICAL_SECTION CRITICAL_SECTION;
可以看到,CRITICAL_SECTION实际上就是RTL_CRITICAL_SECTION,而RTL_CRITICAL_SECTION又是在WinNT.h里定义的:
1: typedef struct _RTL_CRITICAL_SECTION{ 2: PRTL_CRITICAL_SECTION_DEBUGDebugInfo; 3: // 4: //Thefollowingthreefieldscontrolenteringandexitingthecritical 5: //sectionfortheresource 6: // 7: LONG LockCount; 8: LONG RecursionCount; 9: HANDLE OwningThread;//fromthethread'sClientId->UniqueThread 10: HANDLE LockSemaphore; 11: ULONG _PTRSpinCount;//forcesizeon64-bitsystemswhenpacked 12: }RTL_CRITICAL_SECTION,*PRTL_CRITICAL_SECTION;
从上面的定义和注释,聪明的你肯定知道Windows API提供的这几个方法是如何操作CRITICAL_SECTION结构的吧。在这里我们只需要关注OwningThread成员,当有线程进入临界区的时候,这个成员就会指向当前线程的句柄。
说了这么多,也许有人已经厌烦了,不是说好说lock么,怎么说半天Win32 API呢,实际上CLR的lock与Win32 API实现方式几乎是一样的。但CLR并没有提供CRITICAL_SECTION结构,不过CLR提供了同步块,CLR还提供了System.Threading.Monitor类。
实际上使用lock的方式,与下面的代码是等价的:
try{ 2: Monitor.Enter(obj); 3: //… 4: }finally{ 5: Monitor.Exit(obj); 6: }
(以下内容只限制在本文,为了简单,有的说法很片面,更详细的内容会在后面两篇里描述)
当CLR初始化的时候,CLR会初始化一个SyncBlock的数组,当一个线程到达Monitor.Enter方法时,该线程会检查该方法接受的参数的同步块索引,默认情况下对象的同步块索引是一个负数(实际上并不是负数,我这里只是为了叙说方便),那么表明该对象并没有一个关联的同步块,CLR就会在全局的SyncBlock数组里找到一个空闲的项,然后将数组的索引赋值给该对象的同步块索引,SyncBlock的内容和CRITICAL_SECTION的内容很相似,当Monitor.Enter执行时,它会设置SyncBlock里的内容,标识出已经有一个线程占用了,当另外一个线程进入时,它就会检查SyncBlock的内容,发现已经有一个线程占用了,该线程就会等待,当Monitor.Exit执行时,占用的线程就会释放SyncBlock,其他的线程可以进入操作了。
好了,有了上面的解释,我们现在可以判断本文前面给出的几个代码,哪一个是上上选呢?
对于代码2,锁定的对象是作为一个局部变量,每个线程进入的时候,锁定的对象都会不一样,它的SyncBlock每一次都是重新分配的,这个根本谈不上什么锁定不锁定。
对于代码3,一般说来应该没有什么事情,但这个操作却是很危险的,typeof(Singleton)得到的是Singleton的Type对象,所有Singleton实例的Type都是同一个,Type对象也是一个对象,它也有自己的SyncBlock,Singleton的Type对象的SyncBlock在程序中只会有一份,为什么说这种做法是危险的呢?如果在该程序中,其他毫不相干的地方我们也使用了lock(typeof(Singleton)),虽然它和这里的锁定毫无关系,但是只要一个地方锁定了,各个地方的线程都会在等待。
对于代码4,实际上代码4的性质和代码3差不多,如果有一个地方使用了DoSomething方法所在类的实例进行lock,而且恰好如this是同一个实例,那么两个地方就会互斥了。
由此看来只有代码1是上上选,之所以是这样,是因为代码1将锁定的对象作为私有字段,只有这个对象内部可以访问,外部无法锁定。 上面只是从文字上叙说,也许你觉得证据不足,我们就搬来代码作证。 使用ILDasm反编译上面单件模式的Instance属性的代码,其中一段IL代码如下所示:
IL_0007:stloc.1 2: IL_0008:call void [mscorlib]System.Threading.Monitor::Enter(object) 3: IL_000d:nop 4: .try 5: { 6: IL_000e:nop 7: IL_000f:ldsfld class Singleton Singleton::_instance 8: //…. 9: //… 10: } 11: finally 12: { 13: IL_002b:ldloc.1 14: IL_002c:call void [mscorlib]System.Threading.Monitor::Exit(object) 15: IL_0031:nop 16: IL_0032:endfinally 17: }
为了简单,我省去了一部分代码。但是很明显,我们看到了System.Threading.Monitor.Enter和Exit。然后我们拿出Reflector看看这个Monitor到底是何方神圣。哎呀,发现Monitor.Enter和Monitor.Exit的代码如下所示:
[MethodImpl(MethodImplOptions.InternalCall)] 2: public static extern void Enter(objectobj); 3: [MethodImpl(MethodImplOptions.InternalCall),ReliabilityContract(Consistency.WillNotCorruptState,Cer.Success)] 4: public static extern void Exit(objectobj);
只见方法使用了extern关键字,方法上面还标有[MethodImpl(MethodImplOptions.InternalCall)]这样的特性,实际上这说明Enter和Exit的代码是在内部C++的代码实现的。只好拿出Rotor的代码求助了,对于所有"内部实现"的代码,我们可以在sscli20\clr\src\vm\ecall.cpp里找到映射:
FCFuncStart(gMonitorFuncs) 2: FCFuncElement("Enter", JIT_MonEnter) 3: FCFuncElement("Exit", JIT_MonExit) 4: … 5: FCFuncEnd()
原来Enter映射到JIT_MonEnter,一步步的找过去,我们最终到了这里:
Sscli20\clr\src\vm\jithelpers.cpp:
HCIMPL_MONHELPER(JIT_MonEnterWorker_Portable, Object* obj) 2: { 3: //省略大部分代码 4: OBJECTREF objRef = ObjectToOBJECTREF(obj); 5: objRef->EnterObjMonitor(); 6: } 7: HCIMPLEND
objRef就是object的引用,EnterObjMonitor方法的代码如下:
1: void EnterObjMonitor()
2: {
3: GetHeader()->EnterObjMonitor();
4: }
GetHeader()方法获取对象头ObjHeader,在ObjHeader里有对EnterObjMonitor()方法的定义:
1: void ObjHeader::EnterObjMonitor()
2: {
3: GetSyncBlock()->EnterMonitor();
4: }
GetSyncBlock()方法会获取该对象对应的SyncBlock,在SyncBlock里有EnterMonitor方法的定义:
1: void EnterMonitor()
2: {
3: m_Monitor.Enter();
4: }
离核心越来越近了,m_Monitor是一个AwareLock类型的字段,看看AwareLock类内Enter方法的定义:
1: void AwareLock::Enter() 2: { 3: Thread* pCurThread = GetThread(); 4: for (;;) 5: { 6: volatile LONG state = m_MonitorHeld; 7: if (state == 0) 8: { 9: // Common case: lock not held, no waiters. Attempt to acquire lock by 10: // switching lock bit. 11: if (FastInterlockCompareExchange((LONG*)&m_MonitorHeld, 1, 0) == 0) 12: { 13: break; 14: } 15: } 16: else 17: { 18: // It's possible to get here with waiters but no lock held, but in this 19: // case a signal is about to be fired which will wake up a waiter. So 20: // for fairness sake we should wait too. 21: // Check first for recursive lock attempts on the same thread. 22: if (m_HoldingThread == pCurThread) 23: { 24: goto Recursion; 25: } 26: // Attempt to increment this count of waiters then goto contention 27: // handling code. 28: if (FastInterlockCompareExchange((LONG*)&m_MonitorHeld, (state + 2), state) == state) 29: { 30: goto MustWait; 31: } 32: } 33: } 34: // We get here if we successfully acquired the mutex. 35: m_HoldingThread = pCurThread; 36: m_Recursion = 1; 37: pCurThread->IncLockCount(); 38: return; 39: MustWait: 40: // Didn't manage to get the mutex, must wait. 41: EnterEpilog(pCurThread); 42: return; 43: Recursion: 44: // Got the mutex via recursive locking on the same thread. 45: m_Recursion++; 46: }
从上面的代码我们可以看到,先使用GetThread()获取当前的线程,然后取出m_MonitorHeld字段,如果现在没有线程进入临界区,则设置该字段的状态,然后将m_HoldingThread设置为当前线程,从这一点上来这与Win32的过程应该是一样的。如果从m_MonitorHeld字段看,有线程已经进入临界区则分两种情况:第一,是否已进入的线程如当前线程是同一个线程,如果是,则把m_Recursion递加,如果不是,则通过EnterEpilog(pCurThread)方法,当前线程进入线程等待队列。
通过上面的文字描述和代码的跟踪,在我们的大脑中应该有这样一张图了:
总结
现在你应该知道lock背后发生的事情了吧。下一次面试的时候,当别人问你同步块索引的时候,你就可以滔滔不绝的和他论述一番。接下来还有两篇分析同步块的其他作用。
欢迎拍砖,祝编程愉快。
Visual Studio + SOS 小实验
咋一看标题,觉得有些奇怪,同步块索引和HashCode有啥关系呢。从名字上来看离着十万八千里。在不知道细节之前,我也是这样想的,知道细节之后,才发现这两兄弟如此亲密。我们还是先来用Visual Studio + SOS,看一个东西,下面是作为小白兔的示例代码:
using System; 2: public class Program 3: { 4: static void Main() 5: { 6: Foo f = new Foo(); 7: Console.WriteLine(f.GetHashCode()); 8: 9: Console.ReadLine(); 10: } 11: } 12: //就这么一个简单的类 13: public class Foo 14: { 15: 16: }
(使用Visual Studio + SOS调试的时候,请先在项目的属性,调试栏里设置“允许非托管代码调试”)
我们分别在第7行,第9行设置断点,F5运行,当程序停在第一个断点处时(此时f.GetHashCode()还没有执行),我们在Visual Studio的立即窗口里输入:
1: .load sos.dll
2: extension C:\Windows\Microsoft.NET\Framework\v2.0.50727\sos.dll loaded
3: !dso
4: PDB symbol for mscorwks.dll not loaded
5: OS Thread Id: 0x1730 (5936)
6: ESP/REG Object Name
7: 0013ed78 01b72d58 Foo
8: 0013ed7c 01b72d58 Foo
9: 0013efc0 01b72d58 Foo
10: 0013efc4 01b72d58 Foo
使用.load sos.dll加载sos模块,然后使用!dso,我们找到了Foo类型的f对象的内存地址:01b72d58,然后使用Visual Studio调试菜单下的查看内存的窗口,查看f对象头部的内容:
阴影遮住的00 00 00 00就是同步块索引所在的地方了,可以看得出来,此时同步块索引的值还是0(后面会对这个做解释),然后继续F5,程序运行到下一个断点处,这个时候f.GetHashCode()也已调用了,细心的你就会发现,原来对象同步块索引所在的地方的值变了:
Visual Studio这个内存查看器有个很好的功能,对内存变化的以红色标出。我们看到,原来是00 00 00 00变成了现在的4a 73 78 0f。嗯,看来HashCode的获取和同步块索引还是有一些关系的,不然调用GetHashCode方法为什么同步块索引的值会变化呢。再来看看Console.WriteLine(f.GetHashCode())的输出:
不知道着两个值有没有什么关系,我们先把它们都换算成二进制吧。注意,这里的4a 73 78 0f是低位在左,高位在右,下面的十进制是高位再左,低位在右,那4a 73 78 0f实际上就是0x0f78734a了。
0x0f78734a:00001111011110000111001101001010
58225482:00000011011110000111001101001010
Rotor源代码
我们先用0补齐32位,突然发现这两者低26位居然是一模一样的(红色标出的部分),这是巧合还是必然?为了一探究竟只好搬出Rotor的源代码,从源代码里看看是否能发现什么东西。还是遵循老路子,我们先从托管代码开始:
1: public virtual int GetHashCode()
2: {
3: return InternalGetHashCode(this);
4: }
5: [MethodImpl(MethodImplOptions.InternalCall)]
6: internal static extern int InternalGetHashCode(object obj);
在本系列的第一篇文章已经提到过,标记有[MethodImpl(MethodImplOptions.InternalCall)]特性的方法是使用Native Code的方式实现的,在Rotor中,这些代码位于sscli20\clr\src\vm\ecall.cpp文件中:
FCFuncElement("InternalGetHashCode", ObjectNative::GetHashCode) 2: FCIMPL1(INT32, ObjectNative::GetHashCode, Object* obj) { 3: DWORD idx = 0; 4: OBJECTREF objRef(obj); 5: idx = GetHashCodeEx(OBJECTREFToObject(objRef)); 6: return idx; 7: } 8: FCIMPLEND 9: INT32 ObjectNative::GetHashCodeEx(Object *objRef) 10: { 11: // This loop exists because we're inspecting the header dword of the object 12: // and it may change under us because of races with other threads. 13: // On top of that, it may have the spin lock bit set, in which case we're 14: // not supposed to change it. 15: // In all of these case, we need to retry the operation. 16: DWORD iter = 0; 17: while (true) 18: { 19: DWORD bits = objRef->GetHeader()->GetBits(); 20: 21: if (bits & BIT_SBLK_IS_HASH_OR_SYNCBLKINDEX) 22: { 23: if (bits & BIT_SBLK_IS_HASHCODE) 24: { 25: // Common case: the object already has a hash code 26: return bits & MASK_HASHCODE; 27: } 28: else 29: { 30: // We have a sync block index. This means if we already have a hash code, 31: // it is in the sync block, otherwise we generate a new one and store it there 32: SyncBlock *psb = objRef->GetSyncBlock(); 33: DWORD hashCode = psb->GetHashCode(); 34: if (hashCode != 0) 35: return hashCode; 36: 37: hashCode = Object::ComputeHashCode(); 38: 39: return psb->SetHashCode(hashCode); 40: } 41: } 42: else 43: { 44: // If a thread is holding the thin lock or an appdomain index is set, we need a syncblock 45: if ((bits & (SBLK_MASK_LOCK_THREADID | (SBLK_MASK_APPDOMAININDEX << SBLK_APPDOMAIN_SHIFT))) != 0) 46: { 47: objRef->GetSyncBlock(); 48: // No need to replicate the above code dealing with sync blocks 49: // here - in the next iteration of the loop, we'll realize 50: // we have a syncblock, and we'll do the right thing. 51: } 52: else 53: { 54: DWORD hashCode = Object::ComputeHashCode(); 55: 56: DWORD newBits = bits | BIT_SBLK_IS_HASH_OR_SYNCBLKINDEX | BIT_SBLK_IS_HASHCODE | hashCode; 57: 58: if (objRef->GetHeader()->SetBits(newBits, bits) == bits) 59: return hashCode; 60: // Header changed under us - let's restart this whole thing. 61: } 62: } 63: } 64: }
代码很多,不过大部分操作都是在做与、或、移位等。而操作的对象就是这行代码获取的:objRef->GetHeader()->GetBits(),实际上就是获取同步块索引。
想想,在第一个断点命中的时候,同步块索引的值还是0x00000000,那应该是下面这块代码执行:
1: DWORD hashCode = Object::ComputeHashCode();
2: DWORD newBits = bits | BIT_SBLK_IS_HASH_OR_SYNCBLKINDEX | BIT_SBLK_IS_HASHCODE | hashCode;
3: if (objRef->GetHeader()->SetBits(newBits, bits) == bits)
4: return hashCode;
通过Object的ComputeHashCode方法算出一个哈希值来(由于本文不是关注哈希算法的,所以这里不讨论这个ComputeHashCode方法的实现)。然后进行几个或操作(这里还要与原先的bits或操作是为了保留原来的值,说明这个同步块索引还起了别的作用,比如上篇文章的lock),然后将同步块索引中老的位换掉。从这里我们还看不出来什么。不过,如果我们再次对这个对象调用GetHashCode()方法呢?那同步块索引不再为0x00000000,而是0x0f78734a,在来看看几个定义的常量的值:
1: #define BIT_SBLK_IS_HASH_OR_SYNCBLKINDEX 0x08000000
2: #define BIT_SBLK_IS_HASHCODE 0x04000000
3: #define HASHCODE_BITS 26
4: #define MASK_HASHCODE ((1<<HASHCODE_BITS)-1)
从刚才设置hashcode的地方可以看到:DWORD newBits = bits | BIT_SBLK_IS_HASH_OR_SYNCBLKINDEX | BIT_SBLK_IS_HASHCODE | hashCode;
所以开头的两个if都可以通过了,返回的hashcode就是bits & MASK_HASHCODE。
这个MASK_HASHCODE是将1向左移26位=100000000000000000000000000,然后减1=00000011111111111111111111111111(低26位全部为1,高6位为0),然后与同步块索引相与,其实这里的作用不就是为了取出同步块索引的低26位的值么。再回想一下本文开头的那个试验,原来不是巧合啊。
连上上一篇,我们可以看到同步块索引不仅仅起到lock的作用,有时还承担着存储HashCode的责任。实际上同步块索引是这样的一个结构:总共32位,高6位作为控制位,后26的具体含义随着高6位的不同而变化,高6位就像很多小开关,有的打开(1),有的关闭(0),不同位的打开和关闭有着不同的意义,程序也就知道低26位到底是干啥的了。这里的设计真是巧妙,不断占用内存很紧凑,程序也可以灵活处理,灵活扩展。
后记
本篇和上一篇一样,都是单独将独立的内容拿出来,这样可以更简单的来阐述。比如在本文中,我只设想同步块索引做hashcode的存储,这个时候,同步块索引就干干净净(本文前面的试验中先得到的同步块索引就是一个0),但实际中同步块索引可能担任更多的职责,比如既lock,又要获取HashCode,这个时候情况就更复杂,这个在后面一篇文章会综合各种情况更详细的说明。
前面,我用两篇文章详细的讨论了同步块索引在lock和GetHashCode所起的作用。不过两篇文章是分开来讨论的。那可能有人会问,如果我有一个object,它既作为lock的lockHelper对象,也要调用它的GetHashCode方法该怎么办,难道这个同步块索引还可以承担这两个任务么。同步块索引是可以承担这两个任务,但是里面却隐藏着更大的秘密,我们先来看看与同步块索引相关的结构:
大致就是这样的一个结构,一个对象的ObjectHeader中的SyncBlockIndex指向一个Sync Block Entry Table中的一项,这里用虚线表示,是说明这里不是使用指针直接的指向,而是一个索引,这样有个什么好处呢,就是CLR可以随便把这个Table放在哪里,也可以按需增大这个Table的容量,反正我这里使用的是索引而不是指针,是间接的指向。这个Table里的每一项都是一个SyncTableEntry,这个SyncTableEntry有两个字段,第一个字段是一个SyncBlock的指针,指向一个SyncBlock对象。还有一个字段是一个Object指针,有了这个指针CLR就可以跟踪这个SyncBlock是哪个对象的,而且SyncTableEntry和SyncBlock不是放在GC管理的内存中,所以可以根据这个Object*来跟踪对应的对象的实例,当对象死亡后,可以回收对应的SyncBlock和SyncTableEntry,不过这个Object的指针是一个弱引用(弱引用的作用是,如果没有任何强引用引用该对象,则该对象可以被认为是垃圾,允许被垃圾收集)。
不过要注意的是,上面这种结构并不是一个对象“与生俱来”的,也就是说对象刚初始化的时候并不如此。当一个对象刚初始化的时候,在ObjectHeader中,SyncBlockIndex字段是为0的,这个在上一篇文章中的Visual Studio + SOS的实验中我们已经见到过。而如果调用对象的GetHashCode方法,则对象的ObjectHeader中的SyncBlockIndex字段的低26位则用来存储该对象的HashCode,而高6位作为一个标识,表示现在SyncBlockIndex作为存储HashCode之用,具体做法就是将这个SyncBlockIndex与BIT_SBLK_IS_HASHCODE
(#define BIT_SBLK_IS_HASHCODE 0x04000000)作或运算,判别的时候作一下与运算。这个在上一篇文章中也介绍了。而如果调用对象的GetHashCode方法之后,继续将该对象作为lock的对象使用呢?这个时候SyncBlockIndex的低26位会摇身一变,变成一个索引,而且还与BIT_SBLK_IS_HASH_OR_SYNCBLKINDEX (#define BIT_SBLK_IS_HASH_OR_SYNCBLKINDEX 0x08000000)作一下或运算,表示这个SyncBlockIndex现在啊既有存储HashCode之功用,又要作为lock的对象。
那既然这低26位变成了索引,那原来的HashCode跑到哪里去了呢?这个就要一探SyncBlock的结构了:
我们看到最后一个字段,这个字段就是如果SyncBlockIndex还做其他用途是,CLR会将计算所得的hashcode放到这里。而如果对象只作lock对象使用,而没有调用GetHashCode方法,则这个字段为0。根据调用的顺序,这个m_dwHashCode的设置有两种方式:
1、已经调用了GetHashCode方法,然后作lock之用,那这里的m_dwHashCode就是之前存储在SyncBlockIndex中的低26位。
2、先作lock之用,然后调用GetHashCode,那m_dwHashCode就是当时新生成的HashCode,然后放在这里的。
从图中我们还有ADIndex这么一个字段,这个字段是表示当前这个对象属于哪个AppDomain,实际上这个字段也可以在SyncBlockIndex里设置,但是如果SyncBlockIndex要担负别的责任,比如该对象作为lock对象时,ADIndex就在SyncBlock里这个字段设置了。关于为什么需要这个ADIndex我现在还没弄清楚,等我弄明白了,再来更新这篇文章。
SyncBlock的第一个字段是AwareLock,实际上这个东西和我第一篇文章中提到的CRITICAL_SECTION结构是一样的,具体细节可以参见“揭示同步索引块(上)-从lock开始”这篇文章。
而这里的SLink字段有两个作用:
1、当SyncBlock是活动的时候,这个字段将作为一个队列,保存在这里排队的线程(作为lock对象时)。
2、当SyncBlock被回收时,这个字段就作为空闲的SyncBlock列表。
好了,同步块索引的相关用途和结构在上、中、下这三篇文章中基本讨论完了。没想到这么一个小小的,不起眼的同步块索引却有这么一番作用。从这个同步块索引的使用上,也可以看得出来微软的CLR
Team在设计的时候,对内存、性能可谓斤斤计较,充分的利用每一个bit,一个bit的不同就会表示不同的作用。