1.變量的線程安全性與變量的作用域有關。
2.對象
對象是類型的實例
在創建對象時,會單獨有內存區域存儲對象的屬性和方法。所以,一個類型的多個實例,在執行時,只要沒有靜態變量的參與,應該都是線程安全的。
這跟我們調試狀態下,是不一樣的。調試狀態下,如果多個線程都創建某實例的對象,每個對象都調用自身方法,在調試是,會發現是訪問的同一個代碼,多個線程是有沖突的。但是,真正的運行環境是線程安全的。
以銷售員為例,假設產品是充足的,多個銷售員,銷售產品,調用方法:Sale(),其是線程安全的。
但是,如果涉及到倉庫,必須倉庫有足夠的產品才能進行銷售,這時,多個銷售人員就有了臨界資源:倉庫。
在這里我們只討論對象的普通方法。至於方法傳入的參數,以及方法內對靜態變量操作的,這里需要根據參數和靜態變量來判定方法的線程安全性。
銷售員案例:
using System; using System.Threading; namespace MutiThreadSample.Sale { /// <summary> /// 銷售 /// </summary> public class Saler { /// <summary> /// 名稱 /// </summary> public string Name { get; set; } /// <summary> /// 間隔時間 /// </summary> public int IntervalTime { get; set; } /// <summary> /// 單位時間銷售運量 /// </summary> public int SaleAmount { get; set; } /// <summary> /// 銷售 /// </summary> public void Sale() { Console.WriteLine("銷售:{0} 於 {1} 銷售產品 {2}", this.Name, DateTime.Now.Millisecond, this.SaleAmount); Thread.Sleep(IntervalTime); } /// <summary> /// 銷售 /// </summary> /// <param name="interval">時間間隔</param> public void Sale(object obj) { WHouseThreadParameter parameter = obj as WHouseThreadParameter; if (parameter != null) { while (parameter.WHouse != null && parameter.WHouse.CanOut(this.SaleAmount)) { parameter.WHouse.Outgoing(this.SaleAmount); Console.WriteLine("Thread{0}, 銷售:{1} 於 {2} 銷售出庫產品 {3}", Thread.CurrentThread.Name, this.Name, DateTime.Now.Millisecond, this.SaleAmount); Thread.Sleep(this.IntervalTime); } } } } }
3靜態類型
已經講了類的實例--對象的多線程安全性問題。這里只討論類型的靜態變量和靜態方法。
當靜態類被訪問的時候,CLR會調用類的靜態構造器(類型構造器),創建靜態類的類型對象,CLR希望確保每個應用程序域內只執行一次類型構造器,為了做到這一點,在調用類型構造器時,CLR會為靜態類加一個互斥的線程同步鎖,因此,如果多個線程試圖同時調用某個類型的靜態構造器時,那么只有一個線程可以獲得對靜態類的訪問權,其他的線程都被阻塞。第一個線程執行完 類型構造器的代碼並釋放構造器之后,其他阻塞的線程被喚醒,然后發現構造器被執行過,因此,這些線程不再執行構造器,只是從構造器簡單的返回。如果再一次調用這些方法,CLR就會意識到類型構造器被執行過,從而不會在被調用。
調用類中的靜態方法,或者訪問類中的靜態成員變量,過程同上,所以說靜態類是線程安全的。
最簡單的例子,就是數據庫操作幫助類。這個類的方法和屬性是線程安全的。
using System; namespace MutiThreadSample.Static { public class SqlHelper { /// <summary> /// 數據庫連接 /// </summary> private static readonly string ConnectionString = ""; /// <summary> /// 執行數據庫命令 /// </summary> /// <param name="sql">SQL語句</param> public static void ExcuteNonQuery(string sql) { //執行數據操作,比如新增、編輯、刪除 } } }
但是,對於靜態變量其線程安全性是相對的,如果多個線程來修改靜態變量,這就不一定是線程安全的。而靜態方法的線程安全性,直接跟傳入的參數有關。
總之:
針對變量、對象、類型,說線程安全性,比較籠統,在這里,主要是想讓大家明白,哪些地方需要注意線程安全性。對於變量、對象(屬性、方法)、靜態變量、靜態方法,其線程安全性是相對的,需要根據實際情況而定。
萬劍不離其宗,其判定標准:是否有臨界資源。
4、集合類型是線程安全的嗎?
常用的集合類型有List、Dictionary、HashTable、HashMap等。在編碼中,集合應用很廣泛中,常用集合來自定義Cache,這時候必須考慮線程同步問題。
默認情況下集合不是線程安全的。在System.Collections 命名空間中只有幾個類提供Synchronize方法,該方法能夠超越集合創建線程安全包裝。但是,System.Collections命名空間中的所有類都提供SyncRoot屬性,可供派生類創建自己的線程安全包裝。還提供了IsSynchronized屬性以確定集合是否是線程安全的。但是ICollection泛型接口中不提供同步功能,非泛型接口支持這個功能。
Dictionary(MSDN解釋)
此類型的公共靜態(在 Visual Basic 中為 Shared)成員是線程安全的。 但不保證所有實例成員都是線程安全的。
只要不修改該集合,Dictionary<TKey, TValue> 就可以同時支持多個閱讀器。 即便如此,從頭到尾對一個集合進行枚舉本質上並不是一個線程安全的過程。 當出現枚舉與寫訪問互相爭用這種極少發生的情況時,必須在整個枚舉過程中鎖定集合。 若允許多個線程對集合執行讀寫操作,您必須實現自己的同步。
很多集合類型都和Dictionary類似。默認情況下是線程不安全的。當然微軟也提供了線程安全的Hashtable.
HashTable
Hashtable 是線程安全的,可由多個讀取器線程和一個寫入線程使用。 多線程使用時,如果只有一個線程執行寫入(更新)操作,則它是線程安全的,從而允許進行無鎖定的讀取(若編寫器序列化為 Hashtable)。 若要支持多個編寫器,如果沒有任何線程在讀取 Hashtable 對象,則對 Hashtable 的所有操作都必須通過 Synchronized 方法返回的包裝完成。
從頭到尾對一個集合進行枚舉本質上並不是一個線程安全的過程。 即使某個集合已同步,其他線程仍可以修改該集合,這會導致枚舉數引發異常。 若要在枚舉過程中保證線程安全,可以在整個枚舉過程中鎖定集合,或者捕捉由於其他線程進行的更改而引發的異常。
線程安全起見請使用以下方法聲明
/// <summary> /// Syncronized方法用來創造一個新的對象的線程安全包裝 /// </summary> private Hashtable hashtable = Hashtable.Synchronized(new Hashtable());
在枚舉讀取時,加lock,這里lock其同步對象SyncRoot
/// <summary> /// 讀取 /// </summary> public void Read() { lock(hashtable.SyncRoot) { foreach (var item in hashtable.Keys) { Console.WriteLine("Key:{0}",item); } } }