C#: 雙檢鎖 (Double Checked Locking)

以下內容是我在公司作爲新人培訓講師時對於作業的一次評價,簡單介紹了雙解鎖的作用,可以作爲一個簡單的參考。

大家可以思考這樣一個問題,一個程序可以對應多少個日誌文件?對於我們這個小程序來說1個就夠了,很多同學在設計Logger類的時候都是在構造方法或初始化方法中生成日誌文件的,也就是說,這基本上等價於一個Logger的實例對應一個新的日誌文件(或重新對同一文件重新開啓流)。

[csharp]
Logger myLogger = new Logger(@“D:my.log”);
[/csharp]

如何才能阻止Logger被隨意的new出實例呢?我們可以修改Logger的構造方法,讓構造方法成爲private的,這樣就能實現誰都不能new出Logger實例的目的了。但是,訪問修飾符(如private)只是影響類之外的使用,對於Logger類的內部,是不會受到private的影響的,也就是說,我們依然可以在Logger類中使用new來創建實例,這正是我們想要的,我們可以爲用戶提前創建好一個實例,並作爲這個類的靜態成員存在,從而得到這樣一個Logger類:

[csharp]
public class Logger
{
private static Logger instance = new Logger();
private Logger() { }
public static Logger GetInstance()
{
return instance;
}
}
[/csharp]

通過以上的代碼,我們就可以使用GetInstance() 方法來獲取被提前創建出來的Logger類的實例,而且每次調用GetInstance() 所獲得到的對象都是同一個實例。這種方式就叫做單例模式。

單例模式在實現上分爲兩種,餓漢模式和懶漢模式,上邊的實現就是餓漢模式,它在類初始化的過程中就已經把單例instance對象創建好了,而另一種方式則是現用現加載(延遲加載)也就是懶漢模式,如下所示:

[csharp]
public class Logger
{
private static Logger instance;
private Logger() { }
public static Logger GetInstance()
{
if (instance == null)
{
instance = new Logger();
}
return instance;
}
}
[/csharp]

但是這種實現存在線程安全問題的,例如現在有A、B兩個線程,當A線程調用了GetInstance() 方法,並在黃色位置處進行了實例的空判別,並且進入了if邏輯,而這是發生了線程切換(線程切換是不可預知的,隨機發生的),B線程也調用了GetInstance() 方法,其在黃色位置處也進行了判空操作,而A線程並沒有完成new操作,所以B線程依然進入了if體內,準備new出實例。隨後,假設線程切換回A,A創建出實例並返回了實例A,而後B線程繼續,B有new除了一個實例而返回了另一個實例。那麼,對於A和B線程,他們所得到的實例就是不同的實例了。爲了避免這種情況的發生,我們需要爲其添加一個鎖,來實現線程安全。

[csharp]
public static Logger GetInstance()
{
lock (initLockHelper)
{
if (instance == null)
{
instance = new Logger();
}
}
return instance;
}
[/csharp]

但是,這個鎖的目的是爲了防止首次創建對象時發生的線程問題而增加的,對於之後的更多時間裏,我們是不需要再進行加鎖操作的,這個操作的資源消耗還是比較大的,因此,我們需要在lock之前先一次檢查一下instance是否爲null:

[csharp]
public static Logger GetInstance()
{
if (instance == null)
{
lock (initLockHelper)
{
if (instance == null)
{
instance = new Logger();
}
}
}
return instance;
}
[/csharp]

這種鎖機制我們稱爲 雙檢鎖 (Double Checked Locking)機制,這樣既保證了效率,又保證了線程安全。當我們的對象是一個輕量級類型時(類中沒有太多的資源,比較簡單)這是應該優先考慮使用餓漢模式,而對於類型複雜、資源佔用較多的對象,可以考慮現用現加載,即懶漢模式。

除了上述介紹的單例模式,其實還有多例模式,我們可以在Logger類中維護一個Dictionary對象,其中的成員就是具體的一個個實例,我們可以指定一個名字來獲得對應的對象,比如名爲 ModuleALogger、和ModuleBLogger分別對應兩個不同的實例,隨後可以通過Logger GetInstance(string instanceName) 來獲得具體的實例。

關於Logger的實現,以下是一個簡單示例:

[csharp]
namespace Common.LogHelper
{
#region using directives

using System;
using System.IO;
using System.Text;

#endregion using directives

/// <summary>
/// 日誌記錄類,內容將會以UTF-16編碼保存
/// </summary>

public class FileLogHelper : ILogHelper
{
private static FileLogHelper logHelper;

private static readonly object initLockHelper = new object();
private static readonly object writeLockHelper = new object();
private static readonly object disposeLockHelper = new object();
private FileStream fileStream;

/// <summary>
/// 定義是否將日誌消息輸出至終端屏幕
/// </summary>
private Boolean isShowMsg;

/// <summary>
/// 日誌文件的位置
/// </summary>
private String logFilePath;

private StreamWriter streamWriter;

private FileLogHelper()
{
}

public String LoggerFullPath
{
get { return this.logFilePath; }
}

/// <summary>
/// 初始化日誌記錄器,在指定位置創建日誌文件
/// </summary>
/// <param name="logFileSavePath">日誌文件指定的位置及名稱</param>
/// <param name="showMsgToScreen">是否同時將信息顯示在終端窗口</param>
/// <returns>是否成功生成</returns>
public void InitLogHelper(String logFileSavePath, Boolean showMsgToScreen = false)
{
if (String.IsNullOrEmpty(logFileSavePath))
{
throw new ArgumentNullException("logFileSavePath");
}
try
{
// 判斷指定目錄是否存在,不存在則自動生成
var logDirPath = Path.GetDirectoryName(logFileSavePath);
if (logDirPath == null)
{
throw new ArgumentNullException("logFileSavePath");
}
if (!Directory.Exists(logDirPath))
{
Directory.CreateDirectory(logDirPath);
}
this.logFilePath = logFileSavePath;
this.isShowMsg = showMsgToScreen;
if (!File.Exists(logFileSavePath))
{
File.Create(logFileSavePath).Close();
}
this.fileStream = new FileStream(this.logFilePath, FileMode.Append);
this.streamWriter = new StreamWriter(this.fileStream, Encoding.Unicode);
this.WriteLog(@"Initial Log Writer Successful.");
}
catch (Exception ex)
{
throw new Exception(@"Create Log File Fail.", ex);
}
}

/// <summary>
/// 向日志文件中追加日誌消息
/// </summary>
/// <param name="logText">日誌的消息內容</param>
/// <param name="logType">消息的類型</param>
/// <returns>日誌是否添加成功</returns>
/// <exception cref="System.ArgumentNullException" />
/// <exception cref="System.Exception" />
public void WriteLog(String logText, LogType logType = LogType.Info)
{
lock (writeLockHelper)
{
if (String.IsNullOrEmpty(this.logFilePath))
{
throw new Exception(@"Please initial FileLogHelper at first.");
}
try
{
String infoText;
switch (logType)
{
case LogType.Error:
infoText = "X" + DateTime.Now + "tProgram Error.t" + logText;
break;

case LogType.Warning:
infoText = "#" + DateTime.Now + "tProgram Warning.t" + logText;
break;

case LogType.Info:
infoText = "@" + DateTime.Now + "tProgram Info.t" + logText;
break;

case LogType.Debug:
infoText = "*" + DateTime.Now + "t*DEBUG INFO*t" + logText;
break;

default:
infoText = "X" + DateTime.Now + "tLogHelper Exception, Invalid LogType.t" + logText;
break;
}
if (this.isShowMsg)
{
Console.WriteLine(infoText);
}
this.streamWriter.WriteLine(infoText);
this.streamWriter.Flush();
}
catch (Exception ex)
{
throw new Exception("Can NOT Writting to Log File: " + this.logFilePath, ex);
}
}
}

/// <summary>
/// 釋放日誌記錄器所佔用的相關資源,無需手動調用
/// </summary>
public void Dispose()
{
if (this.streamWriter != null)
{
lock (disposeLockHelper)
{
if (this.streamWriter != null)
{
if (this.streamWriter.BaseStream.CanRead)
{
this.streamWriter.Dispose();
}
this.streamWriter = null;
}
if (this.fileStream != null)
{
if (this.fileStream.CanRead)
{
this.fileStream.Dispose();
}
this.fileStream = null;
}
logHelper = null;
}
}
}

/// <summary>
/// 獲取唯一實例(線程安全)
/// </summary>
/// <returns>日誌記錄器唯一實例</returns>
public static ILogHelper GetInstance()
{
if (logHelper == null)
{
lock (initLockHelper)
{
if (logHelper == null)
{
logHelper = new FileLogHelper();
}
}
}
return logHelper;
}

/// <summary>
/// 析構函數,用於GC的自動調用
/// </summary>
~FileLogHelper()
{
this.Dispose();
}
}
}
[/csharp]

你可以從維基百科上了解更多的內容。維基百科

double_checking

查看原文:http://nap7.com/me/double-checked-locking/

發佈了37 篇原創文章 · 獲贊 5 · 訪問量 5萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章