1. 程式人生 > 實用技巧 >執行緒安全(ThreadSafety)

執行緒安全(ThreadSafety)

這節講一下執行緒安全的例子,以及如何解決執行緒安全問題。

上節提到了執行緒安全的問題,說了一個例子,1000個人搶100張票,這節就從此案例著手,下面先看一下程式碼實現:

private static int tickets = 100;
static void Main(string[] args)
{
    Thread thread = BuyTicket();
    Thread thread2 = BuyTicket();
    Thread thread3 = BuyTicket();
    thread.Name = "Thread1";
    thread2.Name = "Thread2
"; thread3.Name = "Thread3"; thread.Start(); thread2.Start(); thread3.Start(); Thread.Sleep(5000); } private static Thread BuyTicket() { Thread thread = new Thread(() => { while (tickets > 0) { Console.WriteLine($"{Thread.CurrentThread.Name}---------------->買了一張票,票號為:{tickets}
"); tickets--; } }); thread.IsBackground = true; return thread; }

現有三個執行緒,同時訪問共享資源tickets,我們先來看一下執行結果:

100賣出了三次,這就是很明顯的執行緒安全問題,也就是說,他們都同時進入到了while塊中,同時拿到了tickets為100的值,所以我們解決執行緒安全問題,就要從此處下手,讓執行緒訪問共享資料的時候,同一時刻只能有一個執行緒去訪問。

lock鎖

解決執行緒安全的方法就是加鎖(同步鎖,互斥鎖),現在將程式碼改一下,使其執行緒安全:

private static object o = new object();
private static int tickets = 100;
static void Main(string[] args)
{
    Thread thread = BuyTicket();
    Thread thread2 = BuyTicket();
    Thread thread3 = BuyTicket();
    thread.Name = "Thread1";
    thread2.Name = "Thread2";
    thread3.Name = "Thread3";
    thread.Start();
    thread2.Start();
    thread3.Start();
    Thread.Sleep(5000);
}
private static Thread BuyTicket()
{
    Thread thread = new Thread(() =>
      {
          while (true)
          {
              lock (o)
              {
                  if (tickets > 0)
                  {
                      Console.WriteLine($"{Thread.CurrentThread.Name}---------------->買了一張票,票號為:{tickets}");
                      tickets--;
                  }
              }
          }
      });
    thread.IsBackground = true;
    return thread;
}

while塊中,我加上了一個lock塊,它需要一個Object型別的引數作為同步物件,被lock塊包住的程式碼,在同一時間只能有一個執行緒訪問,看一下執行結果(方便檢視,我將數量改為了30):

可以看到,執行緒安全問題已經解決。我們再來看一下同步物件:

 lock (object obj){}

lock塊,它需要一個object型別的引數作為同步物件,也就是說,執行緒走到這裡,會先看看這個同步物件是不是被佔用著,如未被佔用,則進入,否則執行緒阻塞,直到同步物件被解除佔用,注意,多個執行緒,要使用一個同步物件,不然,一個執行緒訪問一個單獨的同步物件,那跟沒加鎖一樣,另外,根據多型性,這個同步物件可以是任意物件,因為object是所有類的父類,但是string型別不可用,這點要注意。

Monitor鎖

monitor鎖的用法跟lock差不多,請看如下程式碼:

while (true)
{
    Monitor.Enter(o);
    if (tickets > 0)
    {
        Console.WriteLine($"{Thread.CurrentThread.Name}---------------->買了一張票,票號為:{tickets}");
        tickets--;
    }
    Monitor.Exit(o);
}

monitor將程式碼塊改為了enter和exit兩個方法,也是需要同步物件。

Mutex互斥鎖

互斥鎖是一個互斥的同步物件,同一時間有且僅有一個執行緒可以獲取它。跟monitor一樣,也是通過兩個方法控制的,具體用法請看下面的程式碼:

private static Mutex mutex = new Mutex();
private static Thread BuyTicket3()
{
    Thread thread = new Thread(() =>
      {
          while (true)
          {
              mutex.WaitOne();//等待
              if (tickets > 0)
              {
                  Console.WriteLine($"{Thread.CurrentThread.Name}---------------->買了一張票,票號為:{tickets}");
                  tickets--;
              }
              mutex.ReleaseMutex();//解除
          }
      });
    thread.IsBackground = true;
    return thread;
}

死鎖

如果濫用執行緒鎖,容易出現死鎖的問題,什麼是死鎖呢?比如有兩個執行緒T1,T2,它們共用兩個同步鎖L1,L2,T1先走L1,T2先走L2,T1下一步走L2,T2下一步走l1,這樣這兩個執行緒各種握著對方的下一步鎖,一直阻塞最後誰也走不了。或者使用像monitor這樣的鎖,突然出現異常,Exit方法來不及執行,也會死鎖,其它的執行緒也會一直阻塞。下面來演示一下:

 private static Thread BuyTicket2()
{
    Thread thread = new Thread(() =>
      {
          try
          {
              while (true)
              {
                  Monitor.Enter(o);
                  throw new Exception("THREAD DEAD!");
                  if (tickets > 0)
                  {
                      Console.WriteLine($"{Thread.CurrentThread.Name}---------------->買了一張票,票號為:{tickets}");
                      tickets--;
                  }
                  Monitor.Exit(o);
              }
          }
          catch{}          
      });
    thread.IsBackground = true;
    return thread;
}

執行結果如下:

因為一開始執行緒就直接死鎖,其它的執行緒無法繼續執行,會一直阻塞。

這是我的公眾號二維碼,獲取最新文章,請關注此號