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/

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值