说到lock锁,我相信在座的各位没有不会用的,而且还知道怎么用不会出错,但让他们聊一聊为什么可以锁住,都说人以群分,大概就有了下面低中高水平的三类人吧。

第一类人

将lock对象定义成static,这样就能让多个线程看到同一个对象,以此实现线程间互斥和保证同步,如果再深问为什么?就怕遮遮掩掩的说好像每个实例都有一个同步块索引,再展开的话就顶不住了,反正大家都这么写,我也不敢问,我也不会说,如果上代码,只能这样丢给你。

public class Program
{
    public static object lockMe = new object();

    public static void Main(string[] args)
    {
        var task1 = Task.Factory.StartNew(() =>
        {
            lock (lockMe)
            {
                //todo
            }
        });

        var task2 = Task.Factory.StartNew(() =>
        {
            lock (lockMe)
            {
                //todo
            }
        });

        Task.WaitAll(task1, task2);
    }
}

第二类人

这类人可能看过CLR via C# 这样类似圣经级著作,而且对相关概念也比较清楚。

1. 清楚‘引用类型’ 在堆上的布局结构及栈上的指针是指向方法表索引(类型对象指针),如下图。

2. 清楚当lock住对象后,它的‘同步块索引’ 和 CLR上的‘同步块数组’是呈现一个关联关系,然后又是一张图。

牛X点:仅仅用了两张图就把这个事情解决的相当完美,读者一看就明白了,然来是每个线程在lock的时候会查看一下对象的同步块索引所映射的同步块数组中的坑中信息来判断是否可以加锁。

不足点:一定要挑刺的话,那就是这类人只是在听别人讲故事,到底是不是真的如此其实自己心里也没谱,只是一味的相信对方的人格魅力,而真正的人,十句话中只有一句假话

第三类人

这类人就会动用资源或者人脉亲自尝试一下是不是如第二类人所描述的那样,操刀的话,最好的工具就是windbg,接下来我就操刀一把。

1. 对‘引用类型’布局结构的补充

现在大家也知道了每个对象都有两个额外开销,就是‘同步块索引’ + '方法表索引',在x86系统中,每个索引各占4字节,而在x64系统中,每个索引各占8字节,因我的系统是x64,按照x64版本测试。

2. 案例代码

有了上面的知识补充,接下来我开两个task,在task中进行lock操作。

namespace ConsoleApp2
{
    public class Program
    {
        public static void Main(string[] args)
        {
            var employee = new Employee();

            Console.WriteLine("步骤一:lock前!!!");
            Console.ReadLine();

            var task1 = Task.Factory.StartNew(() =>
            {
                lock (employee)
                {
                    Console.WriteLine("步骤二:lock1中。。。。");
                    Console.ReadLine();
                }
                Console.WriteLine("步骤二:退出lock1...");
            });

            var task2 = Task.Factory.StartNew(() =>
            {
                lock (employee)
                {
                    Console.WriteLine("步骤二:lock2中。。。。");
                    Console.ReadLine();
                }
                Console.WriteLine("步骤二:退出lock2...");
            });

            Task.WaitAll(task1, task2);
            Console.WriteLine("步骤三:lock后,全部退出!");
            Console.ReadLine();
        }
    }

    public class Employee
    {
        public int a = 1;
        public int b = 2;
    }
}

3. 使用windbg调试

我准备分三步骤实现,lock前,lock中,lock后,然后拿到这三种情况下的dump文件来展示 employee 对象的同步块索引 和 CLR全局同步块数组实时情况。

<1 style="box-sizing: border-box;"> lock前

先把程序跑起来,再从任务管理器中生成dump文件。

!threads -> ~0s -> !clrstack -l 这三个命令是为了寻找主线程栈上的局部变量 employee 的内存地址。

0:000> !threads
ThreadCount:      2
UnstartedThread:  0
BackgroundThread: 1
PendingThread:    0
DeadThread:       0
Hosted Runtime:   no
                                                                                                        Lock  
       ID OSID ThreadOBJ           State GC Mode     GC Alloc Context                  Domain           Count Apt Exception
   0    1 40b8 00000235222457f0    2a020 Preemptive  0000023523F76D00:0000023523F77FD0 000002352223b0f0 1     MTA
   6    2 44c8 00000235222705f0    2b220 Preemptive  0000000000000000:0000000000000000 000002352223b0f0 0     MTA (Finalizer)
0:000>  ~0s
ntdll!ZwReadFile+0x14:
00007ffa`bd7baa64 c3              ret
0:000> !clrstack -l
OS Thread Id: 0x40b8 (0)
        Child SP               IP Call Site
0000005f721fe748 00007ffabd7baa64 [InlinedCallFrame: 0000005f721fe748] Microsoft.Win32.Win32Native.ReadFile(Microsoft.Win32.SafeHandles.SafeFileHandle, Byte*, Int32, Int32 ByRef, IntPtr)
0000005f721fe748 00007ffaa5d7b7e8 [InlinedCallFrame: 0000005f721fe748] Microsoft.Win32.Win32Native.ReadFile(Microsoft.Win32.SafeHandles.SafeFileHandle, Byte*, Int32, Int32 ByRef, IntPtr)
0000005f721fe710 00007ffaa5d7b7e8 *** ERROR: Module load completed but symbols could not be loaded for mscorlib.ni.dll
DomainNeutralILStubClass.IL_STUB_PInvoke(Microsoft.Win32.SafeHandles.SafeFileHandle, Byte*, Int32, Int32 ByRef, IntPtr)

0000005f721fe7f0 00007ffaa65920cc System.IO.__ConsoleStream.ReadFileNative(Microsoft.Win32.SafeHandles.SafeFileHandle, Byte[], Int32, Int32, Boolean, Boolean, Int32 ByRef)
    LOCALS:
        <no data>
        <no data>
        <no data>
        <no data>
        <no data>
        <no data>

0000005f721fe880 00007ffaa6591fd5 System.IO.__ConsoleStream.Read(Byte[], Int32, Int32)
    LOCALS:
        <no data>
        <no data>

0000005f721fe8e0 00007ffaa5d470f4 System.IO.StreamReader.ReadBuffer()
    LOCALS:
        <no data>
        <no data>

0000005f721fe930 00007ffaa5d47593 System.IO.StreamReader.ReadLine()
    LOCALS:
        <no data>
        <no data>
        <no data>
        <no data>

0000005f721fe990 00007ffaa6738b0d System.IO.TextReader+SyncTextReader.ReadLine()

0000005f721fe9f0 00007ffaa6530d98 System.Console.ReadLine()

0000005f721fea20 00007ffa485d0931 *** WARNING: Unable to verify checksum for ConsoleApp2.exe
ConsoleApp2.Program.Main(System.String[]) [C:\dream\Csharp\ConsoleApp1\ConsoleApp2\Program.cs @ 19]
    LOCALS:
        0x0000005f721feaa8 = 0x0000023523f72dc0
        0x0000005f721feaa0 = 0x0000000000000000
        0x0000005f721fea98 = 0x0000000000000000

0000005f721fecb8 00007ffaa7af6c93 [GCFrame: 0000005f721fecb8]

从最后的LOCALS中可以看到,当前主线程有三个局部变量,依次是:employee,task1,task2,而其中的 0x0000023523f72dc0 就是employee。

!dumpobj 0x0000023523f72dc0 -> !dumpobj 0000023523f72dd8 找到 employee 在堆上的内存区域

0:000>  !dumpobj 0x0000023523f72dc0
Name:        ConsoleApp2.Program+<>c__DisplayClass0_0
MethodTable: 00007ffa484c5af8
EEClass:     00007ffa484c2600
Size:        24(0x18) bytes
File:        C:\dream\Csharp\ConsoleApp1\ConsoleApp2\bin\x64\Debug\ConsoleApp2.exe
Fields:
              MT    Field   Offset                 Type VT     Attr            Value Name
00007ffa484c5bb8  4000003        8 ConsoleApp2.Employee  0 instance 0000023523f72dd8 employee
0:000> !dumpobj 0000023523f72dd8
Name:        ConsoleApp2.Employee
MethodTable: 00007ffa484c5bb8
EEClass:     00007ffa484c2678
Size:        24(0x18) bytes
File:        C:\dream\Csharp\ConsoleApp1\ConsoleApp2\bin\x64\Debug\ConsoleApp2.exe
Fields:
              MT    Field   Offset                 Type VT     Attr            Value Name
00007ffaa57685a0  4000001        8         System.Int32  1 instance                1 a
00007ffaa57685a0  4000002        c         System.Int32  1 instance                2 b

使用菜单 view -> memory 查看 0000023523f72dd8 在堆上的布局,从图上看找的没有错哈。

00000235`23f72dc8 d8 2d f7 23 35 02 00 00 00 00 00 00 00 00 00 00  .-.#5...........
00000235`23f72dd8 b8 5b 4c 48 fa 7f 00 00 01 00 00 00 02 00 00 00  .[LH............

从上面看到,00000235`23f72dd8行的前8个字节就是employee的同步块索引,此时全部是0,好的,记录一下这个状态。

<2 style="box-sizing: border-box;"> lock中

继续在控制台按Enter,从图中可以看到lock1获取到了锁。

使用view -> memory 查看 0000023523f72dd8 内存索引地址,可以看到由原来的全0变成了 0000000007000008,如下图。

然后用 !syncblk -all 把CLR的全局同步块数组调出来,看看是不是占了一个坑位。

0:006> !syncblk -all
Index SyncBlock MonitorHeld Recursion Owning Thread Info  SyncBlock Owner
    1 00000235222af108            0         0 0000000000000000     none    0000023523f77150 System.__ComObject
    2 00000235222af158            0         0 0000000000000000     none    0000023523f77170 System.EventHandler`1[[Windows.Foundation.Diagnostics.TracingStatusChangedEventArgs, mscorlib]]
    3 00000235222af1a8            0         0 0000000000000000     none    0000023523f771b0 Windows.Foundation.Diagnostics.TracingStatusChangedEventArgs
    4 00000235222af1f8            0         0 0000000000000000     none    0000023523f79458 Microsoft.Win32.UnsafeNativeMethods+ManifestEtw+EtwEnableCallback
    5 00000235222af248            0         0 0000000000000000     none    0000023523f7a158 Microsoft.Win32.UnsafeNativeMethods+ManifestEtw+EtwEnableCallback
    6 00000235222af298            0         0 0000000000000000     none    0000023523f7a2f8 System.Object
    7 00000235222af2e8            3         1 00000235222cb320 56a8   6   0000023523f72dd8 ConsoleApp2.Employee
-----------------------------
Total           7
CCW             1
RCW             2
ComClassFactory 0
Free            0

看到最后一行了没?ConsoleApp2.Employee 占用的坑位编号是7,说明 0000000007000008 和这个 7 做了关联,同时MonitorHeld=3也说明当前有一个持有线程(+1),有一个等待线程(+2),所以这个观点也得到了验证。

<3 style="box-sizing: border-box;"> lock后

继续在控制台Enter,从图中可以看到两个lock都已经结束了。看此时employee会怎样?

然后还是一样查看 0000023523f72dd8 的内存布局情况。

不过奇怪的是对象的同步块索引并没有变,继续查看同步块数组。

0:000> !syncblk -all
Index SyncBlock MonitorHeld Recursion Owning Thread Info  SyncBlock Owner
    1 00000235222af108            0         0 0000000000000000     none    0000023523f77150 System.__ComObject
    2 00000235222af158            0         0 0000000000000000     none    0000023523f77170 System.EventHandler`1[[Windows.Foundation.Diagnostics.TracingStatusChangedEventArgs, mscorlib]]
    3 00000235222af1a8            0         0 0000000000000000     none    0000023523f771b0 Windows.Foundation.Diagnostics.TracingStatusChangedEventArgs
    4 00000235222af1f8            0         0 0000000000000000     none    0000023523f79458 Microsoft.Win32.UnsafeNativeMethods+ManifestEtw+EtwEnableCallback
    5 00000235222af248            0         0 0000000000000000     none    0000023523f7a158 Microsoft.Win32.UnsafeNativeMethods+ManifestEtw+EtwEnableCallback
    6 00000235222af298            0         0 0000000000000000     none    0000023523f7a2f8 System.Object
    7 00000235222af2e8            0         0 0000000000000000     none    0000023523f72dd8 ConsoleApp2.Employee
    8 00000235222af338            0         0 0000000000000000     none    0000023523f76750 System.IO.TextWriter+SyncTextWriter
-----------------------------
Total           8
CCW             1
RCW             2
ComClassFactory 0
Free            0

从各项都是0来看,它已经处于初始化状态了,MonitorHeld=0也表示当前无线程持有ConsoleApp2.Employee,关于对象同步块索引没有变以及数组中的坑位,可能会被CLR后期惰性删除和初始化吧,谁知道呢?

总结

貌似跟踪下来和CLR via C#说的不是那么一致,如果我是对的,那就是重大发现,如果是错的,那就是水平有限,开个玩笑,可能新版本在底层做了进一步优化吧。