在企業級軟件開發過程中,為了改善應用程序的性能需要通常使用對象池來控制對象的實例化。例如,在我們每次需要連接一個數據庫時都需要創建一個數據庫連接,而數據庫連接是非常昂貴的對象。所以,為了節省為每次數據庫調用都實例化一個數據庫連接的資源,我們可以緩存並重用一些創建好的數據庫連接對象並通過節省為每次數據庫調用都創建一個數據庫連接對象的時間和資源來大幅度提高程序性能。
對象池與圖書館很像。圖書館里維護很多書籍。當對某本書的需求增加時,圖書館就會買更多書,否則的話讀者們就會一直使用同一本書。在對象池中,首先我們檢查對象是否已經被創建且被放到池中,如果已經被放到池中,我們就會得到對象池中緩存的對象;如果沒有找到就會創建一個新的對象並放到對象池中以備之后使用。對象池計數廣泛地用於大規模應用程序服務,比如企業級Java組件模型(Enterprise Java Beans Servers, EJB),MTS/COM+, 甚至在.NET Framework中.
在這部分,我們將開發一個數據庫連接池來緩存數據庫連接。創建數據庫連接是很昂貴的。在一個典型的Web應用中可能有幾千個用戶同時訪問站點。如果這些用戶恰好想要訪問數據庫的動態數據而我們繼續為每個用戶創建一個數據庫連接的話,我們將對應用程序的性能帶來負面影響。創建一個新的對象要求更多內存。內存分配會降低應用程序性能,最后的結果是Web站點在分發動態內容時變得非常慢,或者到達一個臨界值導致站點崩潰。連接池維護一個已創建的對象池,所以需要一個數據庫連接的應用程序可以從池中借一個連接並在用完以后還給對象池,而不是創建一個新的數據庫連接。一旦數據發送給一個用戶,對象的數據庫連接就會被收回以備之后使用。
實現對象池
讓我們看一個我們的由類圖描述的數據庫連接池應用。圖 5 顯示了ObjectPool 類和繼承自ObjectPool的DBConnectionSingleton 類。
圖 5
ObjectPool 類
我們先貼出ObjectPool 類的代碼然后開始討論:
/************************************* /* 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.Linq; using System.Text; using System.Collections; using System.Timers; namespace ObjectPoolSample { public abstract class ObjectPool { //Last Checkout time of any object from the pool. private long lastCheckOut; //Hashtable of the check-out objects. private static Hashtable locked; //Hashtable of available objects private static Hashtable unlocked; //Clean-Up interval internal static long GARBAGE_INTERVAL = 90 * 1000; //90 seconds static ObjectPool() { locked = Hashtable.Synchronized(new Hashtable()); unlocked = Hashtable.Synchronized(new Hashtable()); } internal ObjectPool() { lastCheckOut = DateTime.Now.Ticks; //Create a Time to track the expired objects for cleanup. Timer aTimer = new Timer(); aTimer.Enabled = true; aTimer.Interval = GARBAGE_INTERVAL; aTimer.Elapsed += new ElapsedEventHandler(CollectGarbage); } protected abstract object Create(); protected abstract bool Validate(object o); protected abstract void Expire(object o); internal object GetObjectFromPool() { long now = DateTime.Now.Ticks; lastCheckOut = now; object o = null; lock (this) { try { foreach (DictionaryEntry myEntry in unlocked) { o = myEntry.Key; unlocked.Remove(o); if (Validate(o)) { locked.Add(o, now); return o; } else { Expire(o); o = null; } } } catch (Exception) { } o = Create(); locked.Add(o, now); } return o; } internal void ReturnObjectToPool(object o) { if (o != null) { lock (this) { locked.Remove(o); unlocked.Add(o, DateTime.Now.Ticks); } } } private void CollectGarbage(object sender, ElapsedEventArgs ea) { lock (this) { object o; long now = DateTime.Now.Ticks; IDictionaryEnumerator e = unlocked.GetEnumerator(); try { while (e.MoveNext()) { o = e.Key; if ((now - (long)unlocked[o]) > GARBAGE_INTERVAL) { unlocked.Remove(o); Expire(o); o = null; } } } catch (Exception) { } } } } }
ObjectPool 類有兩個重要的方法; GetObjectFromPool(), 從對象池中獲取一個對象, ReturnObjectToPool(), 把對象還給對象池。我們以兩個哈希表實現對象池,一個稱為locked, 另一個稱為unlocked. locked 哈希表包含所有正在使用的對象而unlocked 哈希表包含了所有未被使用且可隨時使用的對象。ObjectPool 還有三個三個必須重載的方法:Create(), Validate() 和 Expire(), 它們必須由繼承類實現。
總而言之,ObjectPool 類中有三個關鍵部分:
使用GetObjectFromPool() 來從對象池中獲取一個對象,當需要向對象池中添加一個對象時必須使用鎖,由於這個過程locked 和 unlocked 哈希表的內容會發生變化而我們不想在這個過程中發生沖突。
使用ReturnObjectToPool() 來把一個對象返回給對象池,同樣需要使用鎖,理由同上。
使用CollectGarbage() 從對象池中清除過期對象,在這個方法中我們遍歷unlocked哈希表以便從對象池中找到並移除過期對象。這個過程中unlocked哈希表的內容可能會發生改變所以我們需要使用鎖來保證這一過程是原子操作。
GetObjectFromPool() 方法中,我們遍歷unlocked哈希表來獲取第一個可用對象。獲得了以后使用Validate() 方法去驗證指定對象。基於不同的緩存對象類型,Validate()方法的實現也可能有很大不同。例如,如果對象是一個數據庫連接,那么繼承對象池的類就需要實現Validate()方法來檢查數據庫連接是打開的還是關閉的。如果對象池對象驗證通過了,我們從unlocked哈希表中移除這個對象並把它放到locked哈希表中。locked 哈希表中的對象表示正在使用的對象。如果驗證失敗,我們就使用Expired()方法把對象注銷。Expire()方法也需要通過繼承類實現並根據不同的緩存對象類型而有不同的實現形式。還是以一個數據庫連接為例,過期對象將關閉數據庫連接。如果沒有找到一個緩存對象,說明unlocked哈希表是空的,我們使用Create()方法創建一個新對象然后把它放入到locked哈希表中。
ReturnObjectToPool() 方法的實現相對簡單一些。我們僅僅需要將對象從locked哈希表中移除並把它放回unlocked哈希表中以備另用。在整個回收過程中,我們不得不考慮應用程序的內存使用情況。對象池與內存使用量成正比。所以,我們緩存的對象越多,就需要使用更多內存。為了控制內存使用量,我們應該周期性地對池中的對象進行垃圾回收處理。這可以通過對池中每個對象加一個超時周期來實現。如果在超時時間內一個緩存對象沒有被使用,那么它將會被作為垃圾回收。結果就是對象池的內存使用量將很大程序上取決於系統負載。
CollectGarbage() 方法用來處理對象池的垃圾回收。這個方法由ObjectPool構造函數中初始化的一個Timer委托進行調用。在我們的例子中,我們通過GARBAGE_COLLECT 常量將垃圾回收時間間隔定位90秒。
我們還沒有實現任何數據庫連接相關的代碼,所以我們假設ObjectPool 類可以用於對.NET Framework 中的所有類型進行緩存。
DBConnectionSingleton 類
DBConnectionSingleton 類實現了一個數據庫連接對象池。這個類的主要目的是為繼承自ObjectPool 類的特定數據庫連接實現Create(), Validate() 和 Expire()方法。這個類也提供BorrowDBConnection() 和 ReturnDBConnection() 方法來從對象池中借出/返還數據庫連接。
DBConnectionSignletion 類的完整代碼片段如下:
/************************************* /* 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.Linq; using System.Text; using System.Data.SqlClient; using System.Data; namespace ObjectPoolSample { public sealed class DBConnectionSingletion : ObjectPool { private DBConnectionSingletion() { } public static readonly DBConnectionSingletion Instance = new DBConnectionSingletion(); private static string connectionString = @"server=(local);Trusted Connection=yes;database=northwind"; public static string ConnectionString { get { return connectionString; } set { connectionString = value; } } protected override object Create() { SqlConnection conn = new SqlConnection(connectionString); conn.Open(); return conn; } protected override bool Validate(object o) { try { SqlConnection conn = (SqlConnection)o; return !conn.State.Equals(ConnectionState.Closed); } catch (SqlException) { return false; } } protected override void Expire(object o) { try { SqlConnection conn = (SqlConnection)o; conn.Close(); } catch (SqlException) { } } public SqlConnection BorrowDBConnection() { try { return (SqlConnection)base.GetObjectFromPool(); } catch (Exception e) { throw e; } } public void ReturnDBConnection(SqlConnection conn) { base.ReturnObjectToPool(conn); } } }
由於你正在處理的是SqlConnection對象,所以Expire()方法用來關閉SqlConnection, Create() 方法用來創建SqlConnection 而 Validate() 則用來檢查SqlConnection 是打開的還是關閉的。使用DBConnectionSigleton 對象實例可以使整個同步問題對客戶端應用程序透明。
為什么要使用單例模式?
Singleton 是一個著名的創建型設計模式,當你需要一個對象僅對應一個實例時通常需要使用它。設計模式一書(ISBN 0-201-70265-7)中對設計單例模式目的定義為保證一個類僅有一個實例,並提供全局唯一的方式來訪問它。為了實現一個單例,我們需要一個私有構造函數以便於客戶端應用程序無論如何都沒法創建一個新對象,使用靜態的只讀屬性來創建單例類的唯一實例。.NET Framework 在JIT 過程中僅當有任何方法使用靜態屬性時才會將其實例化。如果屬性沒有被使用,那么也就不會創建實例。更准確地說,僅當有任何類/方法對類的靜態成員進行調用時才會構造對應單例類的實例。這個特性稱作惰性初始化並把創建對象的過程留給第一次訪問實例屬性的代碼。.NET Framework 保證共享類型初始化時的類型安全。所以我們不需要擔心DBConnectionSingleton對象的線程安全問題,因為在應用程序整個生命周期內金輝創建一個實例。實例靜態屬性維護DBConnectionSingleton類對象的唯一實例。
使用數據庫連接池
現在已經准備好使用數據庫連接池了,下面的代碼片段顯示了如何實例化並使用數據庫連接池:
/************************************* /* 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.Linq; using System.Text; using System.Configuration; using System.Data.SqlClient; namespace ObjectPoolSample { class Program { static void Main(string[] args) { //Initialize the Pool DBConnectionSingletion pool = DBConnectionSingletion.Instance; //Set the ConnectionString of the DatabaseConnectionPool ConnectionStringSettings settings = ConfigurationManager.ConnectionStrings["NorthwindConnectionString"]; DBConnectionSingletion.ConnectionString = settings.ConnectionString; //Borrow the SqlConnection object from the pool SqlConnection conn = pool.BorrowDBConnection(); //Return the Connection to the pool after using it pool.ReturnDBConnection(conn); Console.ReadLine(); } } }
在上面的例子中,我們通過DBConnectionSingletion 類的實例屬性來初始化它的實例。如上面討論的,我們假設使用單例設計模式可以保證我們有且僅有一個DBConnectionSingletion 對象的實例。我們把ConnectionString 屬性設置為本機SQL Server實例上的北風數據庫。現在,我們可以使用對象池的BorrowDBConnection() 方法來從對象池借一個數據庫連接, 然后通過調用對象池的ReturnDBConnection() 方法來返還數據庫連接。如果你真的想看看應用程序池是如何運行的,那么最好的方式就是打開Visual Studio .NET 中的工程並在調試模式下跟蹤上面給出的應用程序代碼。
總結
在企業級計算的多線程世界中同步是一個極其重要的概念。它被廣泛用於數據庫,消息隊列以及Web 服務器等聞名應用上。任何開發多線程應用程序的開發人員都必須對他們的同步概念特別清楚。不是為了讓每個對象都是線程安全的而導致系統不堪重負,而是應該關注死鎖情況並在程序設計之初就解決盡可能多的死鎖問題。理解同步帶來的性能瓶頸問題同樣很重要,因為它將影響應用程序的總體性能。在這一章,除了探討.NET Framework 中自帶的同步特性,我們也開發了兩個有用的應用程序:
一個自定義的線程安全包裝器。在這個例子中,你學到了如何為你的類庫添加原生同步支持並為調用類庫的開發人員提供是否使用同步的選項。這將幫助第三方開發人員關注於他們自己的應用程序而不是類庫的線程安全問題。
一個數據庫連接池。在這個例子中,你開發了可以用於任意相似對象類型的對象池。有了對象池,我們繼續開發了一個繼承自對象池的數據庫連接池。對象池可以用於任意對象。
至此,第三章 使用線程 的內容已經全部介紹完畢,我們學到了如何使用單線程,多線程,如何解決多線程並發問題,多線程並發時可以使用的不同的鎖,以及如何使用對象池。這些都為我們理解多線程及其並發提供了很大幫助,希望第三章能給你開發大規模應用程序時提供一些參考抑或幫助。
下一章我們將介紹 線程設計原則, 敬請期待…