Head First设计模式——单例模式

单例模式是所有设计模式中最简单的模式,也是我们平常经常用到的,单例模式通常被我们应用于线程池、缓存操作、队列操作等等。

单例模式旨在创建一个类的实例,创建一个类的实例我们用全局静态变量或者约定也能办到单例的作用,为什么我们要用单例模式?

接下来我们就从如何形成单例模式,单例模式创建的过程来讲解。

1、单例如何形成

我们平常创建一个对象需要new对象,假如有一个对象ObjectClass我们实例化它。

new ObjectClass()

如果另外一个类要使用ObjectClass则可以再通过new来创建另外一个实例化,如果这个类是public 则我们可以在使用的时候多次实例化对象。

那我们怎么保证类不被其他类实例化,利用private关键字我们可以采用私有构造函数来阻止外部实例化该类。

1
2
3
4
5
6
public class ObjectClass
{
   private ObjectClass()
    {
    }   
}

这样一来我们无法实例化ObjectClass则我们就无法使用它。那我们要怎么实例化呢?

由于私有构造方法我们只能在内部访问,所以我们可以用一个内部方法实例化ObjectClass,为了外部能够访问这个方法我们将这个方法设置成static。

这样做了之后确保返回对象始终是第一次创建的对象,我们用一个私有静态对象来存储实例化的对象,如果对象没创建我们则立即创建,如果已经创建就返回已经创建的对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class ObjectClass
{
    private static ObjectClass singleton;
    private ObjectClass()
    {
    }
 
    public static ObjectClass GetSingletone()
    {
        if (singleton == null)
        {
            singleton = new ObjectClass();
        }
        return singleton;
    }
}

到这里我们的单例模式就形成了,单例模式定义:

单例模式:确保一个类只有一个实例,并提供一个全局访问点。

2、多线程导致单例模式问题

启用多线程测试单例返回对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Program
{
    static void Main(string[] args)
    {
        for (int i = 0; i < 10; i++)
        {
            TestSingleton();
        }
        Console.ReadKey();
    }
 
    public static void TestSingleton()
    {
        Task.Factory.StartNew(new Action(() =>
        {
            var hc = ObjectClass.GetSingletone().GetHashCode();
            Console.WriteLine(hc);
        }));
    }
}

  

如图中做的测试一样,我启了10个线程获得单例对象然后打印对象的HashCode。测试发现有HashCode不一致的情况,证明单例返回的对象并不是只有一个。

因为多线程运行的时候可能会同时进行if (singleton == null)的判断,如果此时singleton变量还没被实例化则可能有多个线程进入到实例化代码,以至于返回的实例化对象不是同一个。

3、解决多线程单例问题

由于多线程导致if检查变量问题,则争对检查问题我们可以有两类解决办法:

①"急切"创建实例,不用延迟实例化做法

急切实例化就是在静态初始化器中创建对象,这样就保证了程序运行阶段单例对象已经创建好,去除if判断。

1
2
3
4
5
6
7
8
9
10
11
12
public class ObjectClass
{
    private static ObjectClass singleton=new ObjectClass();
    private ObjectClass()
    {
    }
 
    public static  ObjectClass GetSingletone()
    {
        return singleton;
    }
}

加锁

为了让创建对象只能有一个线程操作,则我们对创建对象代码进行加锁处理,再次改造GetSingletone方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class ObjectClass
{
    private static ObjectClass singleton = new ObjectClass();
    private static object lockObj = new object();
    private ObjectClass()
    {
    }
 
    public static ObjectClass GetSingletone()
    {
        lock (lockObj)
        {
            if (singleton == null)
            {
                singleton = new ObjectClass();
            }
        }
        return singleton;
    }
}

 加锁对性能有一定的损耗,如果你的系统对性能要求比较高,我们对于加锁的处理还有一种优化方式:双重检查加锁

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public static ObjectClass GetSingletone()
   {
       if (singleton == null)
       {
           lock (lockObj)
           {
               if (singleton == null)
               {
                   singleton = new ObjectClass();
               }
           }
       }
       return singleton;
   }

使用双重检查加锁,则多线程在运行的时候如果已经创建了单例对象后就不会再进入到lock代码段以此减少锁带来的性能损耗。

然后我们再来测试一波,启用50个线程,可以看到输出的HashCode是一致的。

4、总结

回到我们开始讲的为什么不用全局变量或者约定来解决单例问题,因为对于我们开发来说虽然有约定但是我们不能保证每个人都按照约定或者滥用全局变量造成问题。

而使用单例模式能进行更好的自我约定和管理,当然我们也有可能会滥用单例模式,这就需要对它能解决什么问题如何使用深入理解。

设计模式并不是要生搬硬套,而是在需要的时候符合的场景进行合理使用。

虽然单例模式比较简单,但通过分析我们看到问题也不少,要更好的使用需要我们更好的分析,也希望这篇博文对你有些帮助。

posted @   XSpringSun  阅读(750)  评论(1编辑  收藏  举报
编辑推荐:
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
阅读排行:
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· Manus的开源复刻OpenManus初探
· 写一个简单的SQL生成工具
· AI 智能体引爆开源社区「GitHub 热点速览」
· C#/.NET/.NET Core技术前沿周刊 | 第 29 期(2025年3.1-3.9)
点击右上角即可分享
微信分享提示