10,891 次浏览

C#: 双检锁 (Double Checked Locking)

以下内容是我在公司作为新人培训讲师时对于作业的一次评价,简单介绍了双解锁的作用,可以作为一个简单的参考。

大家可以思考这样一个问题,一个程序可以对应多少个日志文件?对于我们这个小程序来说1个就够了,很多同学在设计Logger类的时候都是在构造方法或初始化方法中生成日志文件的,也就是说,这基本上等价于一个Logger的实例对应一个新的日志文件(或重新对同一文件重新开启流)。

Logger myLogger = new Logger(@“D:my.log”);

如何才能阻止Logger被随意的new出实例呢?我们可以修改Logger的构造方法,让构造方法成为private的,这样就能实现谁都不能new出Logger实例的目的了。但是,访问修饰符(如private)只是影响类之外的使用,对于Logger类的内部,是不会受到private的影响的,也就是说,我们依然可以在Logger类中使用new来创建实例,这正是我们想要的,我们可以为用户提前创建好一个实例,并作为这个类的静态成员存在,从而得到这样一个Logger类:

public class Logger
{
    private static Logger instance = new Logger();
    private Logger() { }
    public static Logger GetInstance()
    {
        return instance;
    }
}

通过以上的代码,我们就可以使用GetInstance() 方法来获取被提前创建出来的Logger类的实例,而且每次调用GetInstance() 所获得到的对象都是同一个实例。这种方式就叫做单例模式。

单例模式在实现上分为两种,饿汉模式和懒汉模式,上边的实现就是饿汉模式,它在类初始化的过程中就已经把单例instance对象创建好了,而另一种方式则是现用现加载(延迟加载)也就是懒汉模式,如下所示:

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

但是这种实现存在线程安全问题的,例如现在有A、B两个线程,当A线程调用了GetInstance() 方法,并在黄色位置处进行了实例的空判别,并且进入了if逻辑,而这是发生了线程切换(线程切换是不可预知的,随机发生的),B线程也调用了GetInstance() 方法,其在黄色位置处也进行了判空操作,而A线程并没有完成new操作,所以B线程依然进入了if体内,准备new出实例。随后,假设线程切换回A,A创建出实例并返回了实例A,而后B线程继续,B有new除了一个实例而返回了另一个实例。那么,对于A和B线程,他们所得到的实例就是不同的实例了。为了避免这种情况的发生,我们需要为其添加一个锁,来实现线程安全。

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

但是,这个锁的目的是为了防止首次创建对象时发生的线程问题而增加的,对于之后的更多时间里,我们是不需要再进行加锁操作的,这个操作的资源消耗还是比较大的,因此,我们需要在lock之前先一次检查一下instance是否为null:

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

这种锁机制我们称为 双检锁 (Double Checked Locking)机制,这样既保证了效率,又保证了线程安全。当我们的对象是一个轻量级类型时(类中没有太多的资源,比较简单)这是应该优先考虑使用饿汉模式,而对于类型复杂、资源占用较多的对象,可以考虑现用现加载,即懒汉模式。

除了上述介绍的单例模式,其实还有多例模式,我们可以在Logger类中维护一个Dictionary对象,其中的成员就是具体的一个个实例,我们可以指定一个名字来获得对应的对象,比如名为 ModuleALogger、和ModuleBLogger分别对应两个不同的实例,随后可以通过Logger GetInstance(string instanceName) 来获得具体的实例。

关于Logger的实现,以下是一个简单示例:

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();
        }
    }
}

你可以从维基百科上了解更多的内容。维基百科

double_checking

About nista

THERE IS NO FATE BUT WHAT WE MAKE.

One thought on “C#: 双检锁 (Double Checked Locking)

  1. 想补充单例模式的应用场景:
    一些常见例子是:
    1.Windows的Task Manager(任务管理器)
    2. windows的Recycle Bin(回收站)
    3.网站计数器
    4.应用程序的日志应用
    5.配置文件读取
    6.数据库连接池

    单例模式应用的场景一般发现在以下条件下:
    (1)资源共享的情况下,避免由于资源操作时导致的性能或损耗等。如上述中的日志文件,应用配置。
    (2)控制资源的情况下,方便资源之间的互相通信。如线程池等。【频繁访问 IO 资源的对象,可以考虑用单例】
    (3)创建对象时耗时过多或者耗资源过多,但又经常用到的对象;

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注