以下内容是我在公司作为新人培训讲师时对于作业的一次评价,简单介绍了双解锁的作用,可以作为一个简单的参考。
大家可以思考这样一个问题,一个程序可以对应多少个日志文件?对于我们这个小程序来说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(); } } }
你可以从维基百科上了解更多的内容。维基百科
想补充单例模式的应用场景:
一些常见例子是:
1.Windows的Task Manager(任务管理器)
2. windows的Recycle Bin(回收站)
3.网站计数器
4.应用程序的日志应用
5.配置文件读取
6.数据库连接池
单例模式应用的场景一般发现在以下条件下:
(1)资源共享的情况下,避免由于资源操作时导致的性能或损耗等。如上述中的日志文件,应用配置。
(2)控制资源的情况下,方便资源之间的互相通信。如线程池等。【频繁访问 IO 资源的对象,可以考虑用单例】
(3)创建对象时耗时过多或者耗资源过多,但又经常用到的对象;