题目:设计一个日志管理器(LogManager)的单例模式。要求在整个应用程序中只能存在一个日志管理器实例,通过该实例可以记录日志信息并输出到控制台。
要求:
LogManager类只能有一个实例,并提供全局访问点来获取该实例。
LogManager类在第一次被访问时进行初始化,并记录当前时间作为日志的开始时间。
LogManager类应该提供一个公有的方法来记录日志信息。每条日志信息包含日志时间和日志内容。
LogManager类应该提供一个公有的方法来输出所有日志信息到控制台。
LogManager类的实现应该是线程安全的,也就是说多个线程同时访问该类时,不会出现竞争条件。
LogManager类的代码应该具有良好的可读性和可维护性。
提示:
可以使用静态变量和静态构造函数来实现单例模式。
可以使用线程锁(Monitor)来实现线程安全性。
可以使用队列(Queue)来存储日志信息,在输出时按照先进先出的顺序输出。
参考代码:
using System.Collections.Concurrent;
namespace 单例
{
//sealed全局只有一个实例 防止类被继承
internal sealed class LogManager
{
//volatile 修饰后的变量 如果修改 可以被其他线程知晓
private static volatile LogManager instance;
private static object syncRoot = new object();
private ConcurrentQueue<string> logQueue;
private DateTime startTime;
private LogManager()
{
logQueue= new ConcurrentQueue<string>();
startTime= DateTime.Now;
}
public static LogManager Instance {
get {
if (instance == null)
{
lock (syncRoot)
{
if (instance == null)
{
instance = new LogManager();
}
}
}
return instance;
}
}
public void Log(string message)
{
string log = $"{DateTime.UtcNow}:{message}";
logQueue.Enqueue(log);
}
public void PrintLogs()
{
while (logQueue.TryDequeue(out string log))
{
Console.WriteLine(log);
System.Threading.Thread.Sleep(200);
}
}
}
}
sealed
关键字用于修饰一个类,表示该类不能被继承。当我们希望一个类在整个应用程序中只能存在一个实例时,可以使用单例模式,并将单例类标记为 sealed
,以防止其他类继承它并创建多个实例。
volatile
关键字用于修饰字段或变量,表示该字段或变量在多线程环境下是可见且随时可能被修改的。在多线程程序中,每个线程都有其私有的线程缓存,当一个线程修改一个字段或变量时,如果没有使用 volatile
关键字修饰,其他线程可能无法立即看到该变化,因为它们仍然在使用自己的线程缓存。而使用 volatile
关键字修饰字段或变量时,确保了对该字段或变量的读取与写入操作都是直接在主内存进行的,保证了可见性和一致性。
在上述示例代码中:
sealed
关键字用于修饰 LogManager
类,防止其他类继承它。
volatile
关键字用于修饰 instance
字段,确保在多线程环境下对 instance
的读取和写入都是从主内存中进行的,避免了竞态条件导致的问题。
请注意,在多线程编程中使用 volatile
关键字并不总是足以确保线程安全,特别是在复合操作或更复杂的同步需求的情况下。在这些情况下,通常需要使用更强大的同步工具,如 lock
语句、Monitor
类或 Mutex
等。
在单例模式的实现中,为了确保只有一个实例被创建,可能会遇到多个线程同时通过 if (instance == null)
的判断条件。如果两个线程几乎同时通过了该条件,那么它们都会进入互斥锁(lock)
的代码块,并且可能会导致创建多个实例。
为了解决这个问题,使用双重检查锁定机制。首先,在没有实例创建的情况下,两个线程都会通过第一个 if (instance == null)
的判断条件。然后,通过获取互斥锁(lock)
来确保只有一个线程能够继续执行。随后,当获得锁的线程进入互斥锁(lock)
的代码块时,会再次检查实例是否为空,这是因为在前一个线程进入互斥锁之前,可能已经有另一个线程创建了实例。如果第二次检查时实例仍然为空,那么该线程将负责创建实例,确保只有一个实例被创建并赋值给 instance
字段。
这样做的目的是避免不必要的锁竞争,提高性能。第一次的 if (instance == null)
检查可以快速地返回,而不需要等待互斥锁。只有在需要创建实例时才会获取互斥锁,确保只有一个线程能够创建实例。而第二次的 if (instance == null)
检查是为了防止在另一个线程获得互斥锁和创建实例之前,当前线程已经被阻塞并等待互斥锁结束,这时 instance
可能已经被其他线程创建,避免了创建多个实例的问题。
总结起来,双重检查锁定机制在性能和线程安全之间进行权衡,通过两次 null
值的验证来确保只有一个实例被创建,并且尽可能减少了锁竞争。
注:
移除 internal
访问修饰符:如果您希望其他命名空间的类也能够访问 LogManager
,请将其访问修饰符改为 public
。
修改 logQueue
和 startTime
为实例字段:将这两个字段从 static
修改为实例字段,以避免在静态上下文中引入不必要的同步。
使用 ConcurrentQueue<T>
来替代 Queue<T>:ConcurrentQueue<T>
是线程安全的队列,可以避免手动加锁,并行访问队列。
使用 DateTime.UtcNow
替代 DateTime.Now:
在多线程环境下,使用 UtcNow
能够更准确地获取时间戳。