zoukankan      html  css  js  c++  java
  • 线程系列08,实现线程锁的各种方式,使用lock,Montor,Mutex,Semaphore以及线程死锁

    当涉及到多线程共享数据,需要数据同步的时候,就可以考虑使用线程锁了。本篇体验线程锁的各种用法以及线程死锁。主要包括:

     

    使用lock处理数据同步
    使用Monitor.Enter和Monitor.Exit处理数据同步
    使用Mutex处理进程间数据同步
    使用Semaphore处理数据同步
    线程死锁


    □ 使用lock处理数据同步

     

    假设有一个类,主要用来计算该类2个字段的商,在计算商的方法之内让被除数自减,即被除数有可能为零。使用lock语句块保证每次只有一个线程进入该方法。

        class ThreadSafe
    
        {
    
            static readonly object o = new object();
    
            private static int _val1, _val2;
    
            public ThreadSafe(int val1, int val2)
    
            {
    
                _val1 = val1;
    
                _val2 = val2;
    
            }
    
            public void Calculate()
    
            {
    
                lock (o)
    
                {
    
                    --_val2;
    
                    if (_val2 != 0)
    
                    {
    
                        Console.WriteLine(_val1/_val2);
    
                    }
    
                    else
    
                    {
    
                        Console.WriteLine("_val2为零");
    
                    }
    
                    
    
                }
    
            }
    
        }
    

    ○ new object()创建的对象实例,也被称作同步对象
    ○ 同步对象必须是引用类型
    ○ 同步对象通常是私有的、静态的  

     

    客户端有一个静态字段val2被ThreadSafe的2个实例方法共用。

        class Program
    
        {
    
            private static int val2 = 2;
    
            static void Main(string[] args)
    
            {
    
                ThreadSafe ts1 = new ThreadSafe(2, val2);
    
                ThreadSafe ts2 = new ThreadSafe(2, val2);
    
                Thread[] threads = new Thread[2];
    
                threads[0] = new Thread(ts1.Calculate);
    
                threads[1] = new Thread(ts2.Calculate);
    
                threads[0].Start();
    
                threads[1].Start();
    
                Console.ReadKey();
    
            }
    
        }
    

    36

    ○ 虽然ThreadSafe的2个实例方法共用了客户端静态字段val2,因为有了lock的存在,保证了val2的数据同步
    ○ 使用lock出现异常,需要手动处理

     

    □ 使用Monitor.Enter和Monitor.Exit处理数据同步

     

    把上面的Calculate方法修改为:

            public void Calculate()
    
            {
    
                Monitor.Enter(o);
    
                _val2--;
    
                try
    
                {
    
                    if (_val2 != 0)
    
                    {
    
                        Console.WriteLine(_val1 / _val2);
    
                    }
    
                    else
    
                    {
    
                        Console.WriteLine("被除数为零");
    
                    }
    
                }
    
                finally
    
                {
    
                    Monitor.Exit(o);
    
                }
    
            }
    

    37
    ○ 能得到相同的结果。      
    ○ lock其实是语法糖,其内部的实现逻辑就是Monitor.Enter和Monitor.Exit的实现逻辑

     

    如果把Monitor.Exit注释掉,会发生什么呢?

            public void Calculate()
    
            {
    
                Monitor.Enter(o);
    
                _val2--;
    
                try
    
                {
    
                    if (_val2 != 0)
    
                    {
    
                        Console.WriteLine(_val1 / _val2);
    
                    }
    
                    else
    
                    {
    
                        Console.WriteLine("被除数为零");
    
                    }
    
                }
    
                finally
    
                {
    
                    //Monitor.Exit(o);
    
                }
    
            }

    38
    可见,如果没有Monitor.Exit,会捕捉不到异常。

     

    不过,以上代码还有一些不易察觉的、潜在的问题:如果在执行Monitor.Enter方法的时候出现异常,线程将拿不到锁;如果在Monitor.Enter与try之间出现异常,由于无法执行try...catch语句块,锁得不到释放。

     

    为了解决以上问题, CLR 4.0给出了一个Monitor.Enter的重载方法。

    public static void Enter (object obj, ref bool lockTaken);


    现在,如果在执行Monitor.Enter方法的时候失败,即没有拿到锁,lockTaken就为false,finally语句块中无需释放锁;如果在Monitor.Enter之后出现异常,因为线程拿到了锁,lockTaken就为true,最后在finally语句块中释放锁。

     

    所以,Calculate方法更健壮的写法为:

            public void Calculate()
    
            {
    
                bool lockTaken = false;
    
                _val2--;
    
                try
    
                {
    
                    Monitor.Enter(o, ref lockTaken);
    
                    if (_val2 != 0)
    
                    {
    
                        Console.WriteLine(_val1 / _val2);
    
                    }
    
                    else
    
                    {
    
                        Console.WriteLine("被除数为零");
    
                    }
    
                }
    
                finally
    
                {
    
                    if (lockTaken)
    
                    {
    
                        Monitor.Exit(o);
    
                    }
    
                }
    
            }

    另外,Monitor还提供了多个静态方法TryEnter的重载,可以指定在某个时间段内获取锁。

     

    □ 使用Mutex处理进程间数据同步

     

    Mutex的作用和lock相似,不过与lock不同的是:Mutex可以跨进程实施线程锁。Mutex有2个重要的静态方法:

    ○ WaitOne:阻止当前线程,如果收到当前实例的信号,则为true,否则为false
    ○ ReleaseMutex:用来释放锁,只有获取锁的线程才可以使用该方法,与lock一样

     

    Mutex一个经典应用就是:同一时间只能允许一个实例出现。

        class Program
    
        {
    
            static Mutex mutex = new Mutex(true,"darren.mutex");
    
            static void Main(string[] args)
    
            {
    
                if (!mutex.WaitOne(2000))//如果找到互拆体,即有另外一个相同的实例在运行着
    
                {
    
                    Console.WriteLine("另外一个实例已经在运行着了~~");
    
                    Console.ReadLine();
    
                }
    
                else//如果没有发现互拆体
    
                {
    
                    try
    
                    {
    
                        RunAnother();
    
                    }
    
                    finally
    
                    {
    
                        mutex.ReleaseMutex();
    
                    }
    
                }
    
            }
    
            static void RunAnother()
    
            {
    
                Console.WriteLine("我是模拟另外一个实例正在运行着~~不过可以按回车键退出");
    
                Console.ReadLine();
    
            }
    
        }
    

    40

    以上是分别2次双击应用程序后的结果。

     

    □ 使用Semaphore处理数据同步

     

    Semaphore可以被形象地看成是一个舞池,比如该舞池最多能容纳100人,超过100,都要在舞池外边排队等候进入。如果舞池中有一个人离开,在外面等候队列中排在最前面的那个人就可以进入舞池。

     

    如果舞池的容量是1,这时候Semaphore就和Mutex与lock很像了。不过,与Mutex和lock不同的是,任何线程都可以释放Semaphore。

        class Program
    
        {
    
            static Semaphore _semaphore = new Semaphore(3,3);
    
            static void Main(string[] args)
    
            {
    
                Console.WriteLine("ladies and gentleman,舞会开始了~~");
    
                for (int i = 1; i <= 5; i++)
    
                {
    
                    new Thread(IWannaDance).Start(i);
    
                }
    
            }
    
            static void IWannaDance(object id)
    
            {
    
                Console.WriteLine(id + "想跳舞");
    
                _semaphore.WaitOne();
    
                Console.WriteLine(id + "进了");
    
                Thread.Sleep(3000);
    
                Console.WriteLine(id + "准备离开舞池了");
    
                _semaphore.Release();
    
            }
    
        }
    

    41
    可见,舞池最多可容纳3人,超过3人都得排队。

     

    □ 线程死锁

     

    有2个线程:线程1和线程2。有2个资源,资源1和资源2。线程1已经拿到了资源1的锁,还想拿资源2的锁,线程2已经拿到了资源2的锁,同时还想拿资源1的锁。线程1和线程2都没有放弃自己的锁,还同时想要另外的锁,这就形成线程死锁。就像2个小孩,手上都有自己的玩具,却还想要对方的玩具,谁也不肯让谁。

     

    举一个银行转账的例子来呈现线程死锁。

     

    首先是银行账户,提供了存款和取款的方法。

        public class Account
    
        {
    
            private double _balance;
    
            private int _id;
    
            public Account(int id, double balance)
    
            {
    
                this._id = id;
    
                this._balance = balance;
    
            }
    
            public int ID
    
            {
    
                get { return _id; }
    
            }
    
            //取款
    
            public void Withdraw(double amount)
    
            {
    
                _balance -= amount;
    
            }
    
            //存款
    
            public void Deposit(double amount)
    
            {
    
                _balance += amount;
    
            }
    
        }
    

     

    其次是用来转账的一个管理类。

        public class AccountManager
    
        {
    
            private Account _fromAccount;
    
            private Account _toAccount;
    
            private double _amountToTransfer;
    
            public AccountManager(Account fromAccount, Account toAccount, double amount)
    
            {
    
                this._fromAccount = fromAccount;
    
                this._toAccount = toAccount;
    
                this._amountToTransfer = _amountToTransfer;
    
            }
    
            //转账
    
            public void Transfer()
    
            {
    
                Console.WriteLine(Thread.CurrentThread.Name + "正在" + _fromAccount.ID.ToString() + "获取锁");
    
                lock (_fromAccount)
    
                {
    
                    Console.WriteLine(Thread.CurrentThread.Name + "已经" + _fromAccount.ID.ToString() + "获取到锁");
    
                    Console.WriteLine(Thread.CurrentThread.Name + "被阻塞1秒");
    
                    //模拟处理时间
    
                    Thread.Sleep(1000);
    
                    Console.WriteLine(Thread.CurrentThread.Name + "醒了,想想获取" + _toAccount.ID.ToString() + "的锁");
    
                    lock (_toAccount)
    
                    {
    
                        Console.WriteLine("如果造成线程死锁,这里的代码就不执行了~~");
    
                        _fromAccount.Withdraw(_amountToTransfer);
    
                        _toAccount.Deposit(_amountToTransfer);
    
                    }
    
                }
    
            }
    
        }
    

    ○ 使用了2个lock,称为"嵌套锁",当一个方法中调用另外的方法,通常使用"嵌套锁"
    ○ 第1个lock下的Thread.Sleep(1000)让线程阻塞1秒,好让另一个线程进来
    ○ 把"正在获取XX锁","已经获取到XX锁"......等状态,打印到控制台上

     

    客户端开2个线程,一个线程账户A向账户B转账,另一个线程账户B向账户A转账。

        class Program
    
        {
    
            static void Main(string[] args)
    
            {
    
                Console.WriteLine("准备转账了");
    
                Account accountA = new Account(1, 5000);
    
                Account accountB = new Account(2, 3000);
    
                AccountManager accountManagerA = new AccountManager(accountA, accountB, 1000);
    
                Thread threadA = new Thread(accountManagerA.Transfer);
    
                threadA.Name = "线程A";
    
                AccountManager accountManagerB = new AccountManager(accountB, accountA, 2000);
    
                Thread threadB = new Thread(accountManagerB.Transfer);
    
                threadB.Name = "线程B";
    
                threadA.Start();
    
                threadB.Start();
    
                threadA.Join();
    
                threadB.Join();
    
                Console.WriteLine("转账完成");
    
            }
    
        }
    

    39

    正如死锁的定义:线程A获取锁1,线程2获取锁2,线程A想获取锁2,同时线程B想获取锁1。结果:线程死锁。

     

    ○ 获取锁和释放锁的过程是相当快的,大概在几十纳秒的数量级
    ○ 线程锁能解决并发问题,但如果持有锁的时间过长,会增加线程死锁的可能

     

     

    总结:
    ○ 同一进程内,在同一时间,只有一个线程获取锁,占用一个资源或一段代码,使用lock或Monitor.Enter/Monitor.Exit
    ○ 同一进程或不同进程内,在同一时间,只有一个线程获取锁,占用一个资源或一段代码,使用Mutex
    ○ 同一进程或不同进程内,在同一时间,规定有限的线程占有一个资源或一段代码,使用Semaphore
    ○ 使用线程锁的时候要注意造成线程死锁,当线程持有锁的时间过长,容易造成线程死锁

     

    线程系列包括:

    线程系列01,前台线程,后台线程,线程同步

    线程系列02,多个线程同时处理一个耗时较长的任务以节省时间

    线程系列03,多线程共享数据,多线程不共享数据

    线程系列04,传递数据给线程,线程命名,线程异常处理,线程池

    线程系列05,手动结束线程

    线程系列06,通过CLR代码查看线程池及其线程

    线程系列07,使用lock语句块或Interlocked类型方法保证自增变量的数据同步

    线程系列08,实现线程锁的各种方式,使用lock,Montor,Mutex,Semaphore以及线程死锁

    线程系列09,线程的等待、通知,以及手动控制线程数量

    线程系列10,无需显式调用线程的情形

  • 相关阅读:
    docker
    mitmproxy
    20145103《JAVA程序设计》课程总结
    20145103第五次实验报告
    20145103《JAVA程序设计》第十周学习总结
    《JAVA程序设计》第九周学习总结
    第四次实验报告
    第三次实验报告
    《java程序设计》第八周学习总结
    20145103 《Java程序设计》第7周学习总结
  • 原文地址:https://www.cnblogs.com/darrenji/p/3989552.html
Copyright © 2011-2022 走看看