【创建型设计模式】C# 设计模式,单例模式。

题目:设计一个日志管理器(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

修改 logQueuestartTime 为实例字段:将这两个字段从 static 修改为实例字段,以避免在静态上下文中引入不必要的同步。

使用 ConcurrentQueue<T> 来替代 Queue<T>:ConcurrentQueue<T> 是线程安全的队列,可以避免手动加锁,并行访问队列。

使用 DateTime.UtcNow 替代 DateTime.Now:在多线程环境下,使用 UtcNow 能够更准确地获取时间戳。

猜你喜欢

转载自blog.csdn.net/csdn2990/article/details/131581571