同样的执行代码,放在单线程中执行,和放在多线程中执行,结果不一样,这就是多线程安全问题。
线程安全问题是怎么来的?
一个语法糖:
0
        public static void TestMonitor()
        {
            string LOCK= "lock";
            List<int> list = new List<int>();
            for (int i = 0; i < 10000; i++) {
                Task.Run(() => {
                    Monitor.Enter(LOCK);
                    try {
                        list.Add(i);
                    }
                    finally { Monitor.Exit(LOCK); } 
                });
            }
            Thread.Sleep(10000);
            Console.WriteLine("listCount:"+list.Count());
            Console.ReadLine();
        }
下面通过几个具体的实例,来讲解一下lock使用中的一些常见的问题。
案例1:
下面还有一个使用场景是这样,不同的实例间,锁住的同一个非静态变量的话,相同实例下的调用是不会并发运行,不同实例间是会并发执行的。
    public class MyLock
    {
        public readonly object OneLock=new object();

        public void LockCase(string caceName,int count)
        {
            for (int i = 0; i < count; i++) {
                string name = caceName;
                Task.Run(() =>
                {
                    lock (OneLock)
                    {
                        Console.WriteLine($"{name},Start");
                        Thread.Sleep(700);
                        Console.WriteLine($"{name},Finish");
                    }
                });
            }
        }
    }
    static void Main(string[] args)
        {

            MyLock myLock1 = new MyLock();            
            MyLock myLock2 = new MyLock();
            myLock1.LockCase("case1", 10);
            myLock2.LockCase("case3",10);
            myLock1.LockCase("case4", 20);
            myLock2.LockCase("case2",15);


            Console.ReadLine();
    }
0
实际运行中,case1和case4是一个实例,case2和case3是一个实例,如上边描述,实际情况也是,case1和case4不会同时运行,因为他们lock的是同一个资源;同理case2和case3也是。但是case1和case2/case3不是同一实例,那么case1与case2/case3可以同时运行,因为他们锁的不是同一资源。
但是,这个仅仅限于public readonly object OneLock=new object(); 这种声明,假如public static readonly object OneLock=new object();也就是资源为静态资源的时候,那么在Main()方法中的不通实例间,其实也是相当于同一资源,即全局唯一,因此不同实例间也不会同时运行。小结一下就是,不同实例的非静态字段,是不同的;同一实例的非静态字段是相同的。如果是静态字段,那么不同实例间字段也是相同的,因为全局唯一。
案例2:
 private readonly string StringLock = "StringLock";


        public void StringLockCase(string caceName, int count)
        {
            for (int i = 0; i < count; i++)
            {
                string name = caceName;
                Task.Run(() =>
                {
                    lock (StringLock)
                    {
                        Console.WriteLine($"{name},Start");
                        Thread.Sleep(700);
                        Console.WriteLine($"{name},Finish");
                    }
                });
            }
        }
0
这里边有一个知识点,就是string变量在栈中的存储特征有一点特殊,享元模式。(但是如果存在字符串拼接的话,就不在遵循享元模式)
在c#中,String的存储方式很特殊,在c#的内存中,在常量区里会分配一块空间叫做String暂存池(常量池),在某些时候,我们的字符串数据是存储在这个常量池中的,而地址依然是存放在栈中。
例如用 String str = "xXXXX" 的方式来创建String变量的话,那么String的值便会存储在String常量池中,在我们以这种方式创建String变量时,编译器会先判断你这个内容有没有已经在常量池出现过了,如果已经出现过,那么不会再在常量池中使用空间来存放一个相同的内容,这个内容只会固定有一个引用,所以在创造相同内容的String的时候,他们的引用都是相同的。又有一种情况:一开始A和B内容相同,就是说A与B的引用都相同时,此时将B的内容更改,那么B的内容在常量池中就会使用另一块空间,那么相应的B的引用也会改变,而A的引用并不会改变,因为A此时还是存储的原来的内容。我们可以来看简易的图解:
0
回到案例中,case1与 case2/case3虽然是不同实例,当然包括base4,但是在调用过程中,已经存在了“StringLock”,那么相当于锁住的是同一个资源。当我们把lock的资源换成其他类型,就不会出现这种问题了,如下图:
0
所以,使用lock时,尽量不要使用string,避免出现不可控的意外情况。
案例3:
public class MyLockGeneric<T>
    {
        private static readonly object MyLock = new object();
        public static void LockCase(string caceName, int count)
        {
            for (int i = 0; i < count; i++)
            {
                string name = caceName;
                Task.Run(() =>
                {
                    lock (MyLock)
                    {
                        Console.WriteLine($"{name},Start");
                        Thread.Sleep(700);
                        Console.WriteLine($"{name},Finish");
                    }
                });
            }

        }
    }
0
泛型的特点,声明的时候不知道具体类型,只是用T作为占位符,调用的时候才知道具体类型,诞生了一个新的类,具体类型来替代T,不同的具体类型,产生的新的类时不同的,不通的类型里面的静态字段当然也不相同,当然,同一种泛型类型,比如都是int,那就是同一个类,也是同一个静态字段。
回到示例中,case1和case2因为T相同,那么实例化之后应该时同一个类,那么静态字段全局唯一。所以case1和case2不会并发执行。
case1/case2 与case3或case4 T不是相同类型,那么实例化之后为不通的类,那么静态字段,在本类型内全局唯一。那么case1/case2 与case3或case4是可以并发执行的。
 
案例4:
   public void LockThis(string caceName, int count)
        {
            for (int i = 0; i < count; i++)
            {
                string name = caceName;
                Task.Run(() =>
                {
                    lock (this)
                    {
                        Console.WriteLine($"{name},Start");
                        Thread.Sleep(700);
                        Console.WriteLine($"{name},Finish");
                    }
                });
            }
        }
        static void Main(string[] args)
        {

            MyLock myLock1 = new MyLock();
            MyLock myLock2 = new MyLock();
            myLock1.LockThis("case1", 5);
            myLock2.LockThis("case2", 5);
            myLock2.LockThis("case3", 5);
            string name = "case4";
            for (int i = 0; i < 5; i++)
            {
                Task.Run(() =>
                {
                    lock (myLock1)
                    {
                        Console.WriteLine($"{name},Start");
                        Thread.Sleep(700);
                        Console.WriteLine($"{name},Finish");
                    }
                });
            }
            Console.ReadLine();
        }
0
如案例中所示,case2和case3为同一实例,那么case2和case3是不会并发运行的。case4锁住的是myLock1这个实例,case1中锁住的是this,其实也是myLock1那么其实case1与case4是不可能并发运行的。
posted on 2022-08-08 18:43  CRUDEngineer  阅读(981)  评论(0编辑  收藏  举报