盡管使用線程同步對線程安全來說是必須的,但是如果沒有用好的話就可能導致死鎖。因此,理解什么是死鎖並知道如何避免死鎖是非常重要的。當兩個或兩個以上的線程等待兩個或多於兩個鎖被釋放然后程序中的邏輯導致鎖永遠都不會被釋放時死鎖就發生了。圖3描述了一個典型的死鎖場景。
圖3
在上圖中,線程1獲得通過進入一個對象的關鍵區域獲得這個對象的鎖L1。在關鍵部分中線程1想要獲取鎖L2。線程2獲得鎖L2同時還想獲得鎖L1。所以,現在線程1無法獲得鎖L2而線程2無法獲得鎖L1,因為這兩個線程彼此擁有對方需要的鎖而又不會釋放它們。結果是兩個線程都進入無限等待或者死鎖。
阻止潛在的死鎖發生的最好的方式是避免在同一時間獲取多個鎖,這種情況不是經常發生。然而,如果必須這樣做的話,你需要一種策略來保證你可以在一個穩定的、定義好的順序獲得多個鎖。這取決於每個程序是如何設計的,不同的同步策略在避免死鎖時可能很不同。沒有一種方法可以用來避免所有類型的死鎖。大多數時候,死鎖不會被檢測到除非程序被部署到一個大規模運行環境中。如果我們能夠在我們程序的測試階段就能發現死鎖的話那么我們可以說是非常幸運的。當然這取於很多因素,開發人員水平,測試人員水平,開發/測試工具,經驗等等。
一個很關鍵的但是經常被忽略的關於鎖定策略部分的是文檔。不幸的是,即使花費很多時間來設計避免死鎖的同步策略,在記錄這個過程的文檔方面也僅僅做了很少的工作。至少,每個方法都應該有一個關於它是如何確定它要獲得的鎖以及描述方法內的關鍵部分的文檔。
讓我們來看一個例子,Deadlock.cs:
/************************************* /* copyright (c) 2012 daniel dong * * author:daniel dong * blog: www.cnblogs.com/danielwise * email: guofoo@163.com * */ using System; using System.Collections.Generic; using System.Text; using System.Threading; namespace Deadlock { class DL { int field1 = 0; int field2 = 0; private object lock1 = new int[1]; private object lock2 = new int[1]; public void First(int val) { lock (lock1) { Console.WriteLine("First: Acquired lock 1: " + Thread.CurrentThread.GetHashCode() + " Now Sleeping."); //Try commenting Thread.Sleep() Thread.Sleep(1000); Console.WriteLine("First: Acquired lock 1: " + Thread.CurrentThread.GetHashCode() + " Now wants lock2."); lock (lock2) { Console.WriteLine("First: Acquired lock 2: " + Thread.CurrentThread.GetHashCode()); field1 = val; field2 = val; } } } public void Second(int val) { lock (lock2) { Console.WriteLine("Second: Acquired lock 2: " + Thread.CurrentThread.GetHashCode()); lock (lock1) { Console.WriteLine("Second: Acquired lock 1: " + Thread.CurrentThread.GetHashCode()); field1 = val; field2 = val; } } } } public class MainApp { DL d = new DL(); public static void Main() { MainApp m = new MainApp(); Thread t1 = new Thread(new ThreadStart(m.Run1)); t1.Start(); Thread t2 = new Thread(new ThreadStart(m.Run2)); t2.Start(); Console.ReadLine(); } public void Run1() { this.d.First(10); } public void Run2() { this.d.Second(10); } } }
Deadlock.cs 的輸出結果如下:
在Deadlock.cs 中,線程t1調用First()方法獲得lock1, 然后睡眠1秒鍾。同時線程t2調用Second()方法並獲得lock2. 然后在嘗試在同一方法內獲得lock1.但是線程1已經擁有lock1, 所以線程t2不得不等待線程1釋放lock1. 當線程t1醒來后,它嘗試獲得lock2。而lock2已經被線程t2獲得所以線程t1只能等待線程t2釋放它。結果是發生了死鎖而程序卡死。把方法First()中的Thread.Sleep()方法注釋掉不會導致死鎖,至少是個臨時解決方案。因為線程t1會在線程t2之前獲得lock2. 但是在真實場景中,Thread.Sleep()時發生的可能是一個連接數據庫操作,導致線程t2在線程t1之前獲得lock2, 最終結果也是死鎖。這個例子告訴我們在任何多線程應用程序中設計出一個好的鎖定方案是多么重要!一個好的鎖定方案可能通過讓所有線程運行在一個定義好的行為中以合作方式獲得鎖。在上面的例子中,線程t1不應該請求鎖除非它被線程t2釋放, 反之亦然。這些決定取決於特定應用場景而不具有一般性。測試各種鎖定方案是同等重要的,因為死鎖通常發生在那些缺少壓力和功能性測試的系統中。
下一篇介紹兩個關於線程安全的例子…