线程基础必知必会(二)(下)

简介: 线程基础必知必会(二)

这里需要注意的是在使用 Thread.Start(para) 方法传递参数时,被调用方法的参数类型必须是 object 类型。如果你觉得上述两种方法都不好,那么你还可以使用 ParameterizedThreadStart 委托,只需把前面行一段代码中的 Thread thread = new Thread(PrintNumber);修改为 Thread thread = new Thread(new ParameterizedThreadStart(PrintNumber));即可。同样利用 ParameterizedThreadStart 委托也需要把被调用方法参数的类型定为 object 类型。

我个人建议大家在调用带参数的方法是使用匿名方法的方式调用,因为如果方法参数存在多个参数是这样调用更加便捷。当然了在遇到方法带有多个参数时你也可以使用自定义类的方式,但是这种方法并不被微软所推荐,而且这种方法代码量较大,为了调用多参数方法而去定义一个类,可以说是相当的鸡肋。


四、lock

当多个线程同时访问同一个对象时,会出现数据不正确的问题,下面我们先通过一个代码看一下这种情况。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using static System.Console;
namespace ThreadLock
{
    class Program
    {
        static void Main(string[] args)
        {
            CountOperating countOperating = new CountOperating();
            Thread thread1 = new Thread(() => update(countOperating));
            Thread thread2 = new Thread(() => update(countOperating));
            thread1.Start();
            thread2.Start();
            thread1.Join();
            thread2.Join();
            WriteLine(countOperating.count);
            Read();
        }
        static void update(CountOperating countOperating)
        {
            for (int i = 0; i < 10000; i++)
            {
                countOperating.Add();
                countOperating.Subtraction();
            }
        }
    }
    class CountOperating
    {
        public int count { get; set; }
        public void Add()
        {
            count++;
        }
        public void Subtraction()
        {
            count--;
        }
    }
}

上述代码我们希望最后输出的结果是 0 ,但是在代码运行后发现输出结果大部分情况并不是 0 ,这时因为我的创建的 CountOperating 类并非线程安全的了类,当多个线程同时调用同一个 CountOperating 实例时,有很大的可能出现如下情况。首先线程1将 count 值加1,这时第二个线程也获取到 count 的值,此时值已经变为1,再次加1后值变为了2,这时第一个线程再次获取到 count 值值为2,第一个线程在获取到值后准备进行减运算,但是第二个线程也获取到了 count 值值也是2,接着第一个线程执行了减操作,此时得到的值是1,然后第二个线程也同样进行了减操作,此时的值依然是1,也就是说我们只执行了一次减法操作,两次加法操作。为了防止这种情况的发生,我们就需要将我们创建的类修改为线程安全的类,也就是说当一个线程调用 CountOperating 实例时其他线程只能等待。因此我们在这里引入了 lock ,lock 关键字可确保当一个线程位于代码的临界区时,另一个线程不会进入该临界区。 如果其他线程尝试进入锁定的代码,则它将一直等待,直到该对象被释放。lock 关键字在块的开始处调用 Enter,而在块的结尾处调用 Exit。 ThreadInterruptedException 引发,如果 Interrupt 中断等待输入 lock 语句的线程。通常,应避免锁定 public 类型,否则实例将超出代码的控制范围。根据前面所说的我们将 CountOperating 代码修改如下即可:

class CountOperating
{
    readonly object obj = new object();
    public int count { get; set; }
    public void Add()
    {
        lock (obj)
        {
            count++;
        }
    }
    public void Subtraction()
    {
        lock (obj)
        {
            count--;
        }
    }
}

五、Monitor

当我们使用 lock 关键字来锁定一个对象时,其他需要访问该对象的线程会处于阻塞状态,需要等到这个对象解锁后才能进行下一步操作,但是这会出现严重的性能问题和死锁的问题,性能问题相关的解决方式我会在后面的文章讲解,这一小节主要是讲解死锁的解决方案。所谓死锁举个例子来说就是线程A锁定了对象A,线程B锁定了对象B,线程A需要对象B释放后才能释放对象A,但是线程B要等到对象A释放后才能释放对象B。这样对象A和对象B永远不会被释放,线程A和线程B就永远在等待。为了解决这个问题微软为我们提供了一个解决方案,利用 Monitor 类来避免死锁,它通过获取和释放排它锁的方式实现多线程的同步问题。实际上在 .NET 中 lock 关键字时 Monitor 类用例的语法糖,lock 是对 Monitor 的 EnterExit 的一个封装,因此 Monitor 类的 Enter() 和 Exit() 方法的组合使用可以用 lock 关键字替代。Monitor 类除了具有 lock 功能还有以下功能:


  1. TryEnter() :解决长期死等的问题,如果一个并发经常发生,并且持续时间很长,使用TryEnter,可以有效防止死锁或者长时间 的等待。
  2. Wait() : 释放对象上的锁,以便允许其他线程锁定和访问该对象。在其他线程访问对象时,调用线程将等待。脉冲信号用于通知等待线程有关对象状态的更改。
  3. Pulse() / PulseAll() : 向一个或多个等待线程发送信号。该信号通知等待线程锁定对象的状态已更改,并且锁的所有者准备释放该锁。等待线程被放置在对象的就绪队列中以便它可以最后接收对象锁。一旦线程拥有了锁,它就可以检查对象的新状态以查看是否达到所需状态。


上面这三个功能我将会在后续的文章中逐步讲解,下面我们先看一下 Monitor 类的基本用法:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
namespace ThreadMonitor
{
    class Program
    {
        static void Main(string[] args)
        {
            object lock1 = new object();
            object lock2 = new object();
            Thread thread = new Thread(() => Lock(lock1, lock2));
            Thread thread2 = new Thread(() => Lock(lock1, lock2));
            thread.Start();
            lock(lock2)
            {
                Thread.Sleep(2000);
                if(Monitor.TryEnter(lock1,5000))
                {
                    Console.WriteLine("获取了被锁定的对象");
                }
                else
                {
                    Console.WriteLine("超时了");
                }
            }
            thread2.Start();
            lock (lock2)
            {
                Thread.Sleep(1000);
                lock (lock1)
                {
                    Console.WriteLine("获取了被锁定的而对象");
                }
            }
        }
        static void Lock(object lock1,object lock2)
        {
            lock(lock1)
            {
                Thread.Sleep(1000);
                lock (lock2);
            }
        }
    }
}

上述代码中的 Lock 方法先锁定了第一个 lock 对象然后一秒钟后有锁定了第二个 lock 对象。值后在 Main 方法中创建了两个线程都调用 Lock 方法,然后通过两种方式锁定第一个 lock 和第二个 lock ,第一种方法我们使用 Monitor.TryEnter 来锁定对象,并设置了超时时间,一旦超时将会输出 “超时了”,但是第中方式我们利用 lock 来锁定对象,这样就创建了一个死锁。


六、线程异常处理

线程也是代码,因此也会出现异常,大部分开发人员的习惯是直接向上抛出异常,这种做法在普通的代码中并不错,向上抛出异常让方法的调用方去处理这个异常,但是在线程中这种做法就是错误的,因为抛出的异常无法在线程之外被检测的,因此我们必须在线程中将异常处理掉,也就是说在异常中必须使用 try…catch 语句块来捕获和处理异常。


七、源码下载

https://github.com/Thomas-Zhu/Multithreading/tree/master/no2/NoTwo

了”,但是第中方式我们利用 lock 来锁定对象,这样就创建了一个死锁。


目录
相关文章
|
2月前
|
Java 调度
Java并发基础-线程简介(状态、常用方法)
Java并发基础-线程简介(状态、常用方法)
26 0
|
3月前
|
存储 安全 Linux
【探索Linux】P.19(多线程 | 线程的概念 | 线程控制 | 分离线程)
【探索Linux】P.19(多线程 | 线程的概念 | 线程控制 | 分离线程)
28 0
|
3月前
|
C#
C#学习系列相关之多线程(二)----Thread类介绍
C#学习系列相关之多线程(二)----Thread类介绍
|
Java API
java并发原理实战(3) -- 线程的中断和初始化
java并发原理实战(3) -- 线程的中断和初始化
214 0
java并发原理实战(3) -- 线程的中断和初始化
|
Java API Spring
java并发原理实战(4) -- 线程的创建方式
java并发原理实战(4) -- 线程的创建方式
112 0
java并发原理实战(4) -- 线程的创建方式
C#编程-147:线程基础
C#编程-147:线程基础
102 0
C#编程-147:线程基础
|
API C#
C#多线程(14):任务基础②
C#多线程(14):任务基础②
190 0
C#多线程(14):任务基础②
|
Java 数据处理
线程基础必知必会(二)(上)
线程基础必知必会(二)
148 0
线程基础必知必会(二)(上)
|
编译器 C# Windows
线程基础必知必会(一)
线程基础必知必会(一)
155 0
线程基础必知必会(一)
|
Java 中间件 程序员
面经手册 · 第20篇《Thread 线程,状态转换、方法使用、原理分析》
大部分考试考的,基本都是不怎么用的。例外的咱们不说😄 就像你做程序开发,尤其在RPC+MQ+分库分表,其实很难出现让你用一个机器实例编写多线程压榨CPU性能。很多时候是扔出一个MQ,异步消费了。如果没有资源竞争,例如库表秒杀,那么其实你确实很难接触多并发编程以及锁的使用。 但!凡有例外,比如你需要开发一个数据库路由中间件,那么就肯定会出现在一台应用实例上分配数据库资源池的情况,如果出现竞争就要合理分配资源。如此,类似这样的中间件开发,就会涉及到一些更核心底层的技术的应用。
229 0
面经手册 · 第20篇《Thread 线程,状态转换、方法使用、原理分析》