使用C# (.NET Core) 实现单体设计模式 (Singleton Pattern)

简介: 本文的概念内容来自深入浅出设计模式一书 由于我在给公司做内培, 所以最近天天写设计模式的文章.... 单体模式 Singleton 单体模式的目标就是只创建一个实例. 实际中有很多种对象我们可能只需要它们的一个实例, 例如: 线程池,缓存, 弹出的对话框, 用于保存设置的类, 用于logging的类, 硬件设备驱动对象等等.

本文的概念内容来自深入浅出设计模式一书

由于我在给公司做内培, 所以最近天天写设计模式的文章....

单体模式 Singleton

单体模式的目标就是只创建一个实例.

实际中有很多种对象我们可能只需要它们的一个实例, 例如: 线程池,缓存, 弹出的对话框, 用于保存设置的类, 用于logging的类, 硬件设备驱动对象等等.

一段对话:

A: 如何创建一个对象?

B: new MyObject()

A: 如果想创建另一个对象, 就再次new MyObject()?

B: 是的

A: 所以说我们有某个类, 我们就可以对它实例化很多次?

B: 是的, 但是它必须是public的类额

A: 如果不是public的呢?

B: 如果不是public的, 那么只有同一个包下的类才能对它实例化, 但是仍然可以实例化多次.

A: 嗯, 很有趣, 你只你可以这样做吗?

B: 没见过, 但是语法是没问题的, 存在即合理.

A: 它是什么意思呢?

B: 我想它不能被实例化吧, 因为它的构造函数是private的啊.

A: 那么, 有没有哪个对象可以使用这个private的构造函数呢?

B: 额, 我认为只有MyClass里面的代码可以调用这个构造函数, 但是感觉那没什么用啊.

A: 为什么没用呢?

B: 因为对类进行实例化, 就是想要用它的实例, 而这样做的话, 别的类也无法对它进行实例化啊. 这是个鸡和蛋的问题: 我可以使用MyClass里面的构造函数, 但是我无法实例化这个对象, 因为其他的类无法使用 "new MyClass()".

A: 你着确实是一种观点, 那么下面代码是什么意思呢?

B: MyClass有一个静态方法, 我们可以这样调用静态方法: MyClass.getInstance();

A: 为什么使用MyClass, 而不是某个对象的名?

B: 因为getInstance()是静态方法; 也就是说, 它是一个类方法, 你需要使用类名来调用方法.

A: 非常有趣, 那么我把实例化代码放里面呢?

B: 确实可以有这种操作...

A: 那么, 现在你认为有第二种方法来实例化对象吗?

B: MyClass.getInstance();

A: 那么你现在能写出只允许创造一个MyClass实例的代码了吗?

B: 应该行.

 

经典单体模式的实现

首先需要有个静态成员变量保留着实例的引用.

然后构造函数必须是私有的.

getInstance()方法可以该类进行实例化, 并且返回该实例.

另外, 该类也可以有其他方法.

里面最重要的一部分代码:

如果该实例引用为null, 那么创建一个实例, 并把这个实例赋給类的那个成员变量. 这里要注意, 如果我们永远不需要这个类的实例, 那么这个类永远也不会被实例化, 这叫做懒初始化.

如果实例引用不是null, 那么就说明之前已经创建过该类的实例了, 那么就返回之前创建的实例就行了.

 

一道巧克力工厂锅炉的题

先看这个类:

开始的时候, 锅炉是空的, 所以也没有煮沸.

fill()方法(填充), 填充锅炉的时候, 锅炉必须是空的, 一旦填满了, 那么empty就改为false, 表示填满了. 刚填满肯定不是煮沸状态, 所以boiled也是false.

drain()方法(抽取), 只有锅炉是满的并且煮沸之后才能抽取巧克力液体, 抽取完了, 锅炉就又空了 empty改为true.

boil()方法(煮), 煮混合液体, 要求锅炉的前提状态必须是满的 empty为false, 并且还没煮沸 boiled为false. 一旦煮沸了, 就把boiled改成true.

这个工序很好, 但是必须保证只有一个锅炉, 那么该怎么做? 请写出代码.

单体模式定义

单体模式保证一个类只有一个实例, 并提供一个全局访问该实例的方法.

类图:

 

其他问题

上面巧克力锅炉那道题你可能写好了, 但是可能会出现这个问题:

锅炉可能在里面有液体的情况下又进行了fill填充动作. 这是怎么回事?

是不是其他线程引起的这个问题?

我们可能有两个线程都在执行这段代码:

那么两个线程调用时是否有重叠, 代码执行是否有交错?  请看下图:

处理多线程问题

为了解决这个多线程的问题问题, 可已使用synchronized方法:

(synchronized是java里的关键字, C#的请参考下面我写的代码)

使用synchronized关键字以后, 每个线程必须等到轮到它的时候才能进入方法. 这样两个线程就不可能同时进入该方法了.

但是这种方法开销很大, 这有时会成为一个问题. 而且可能比你想的更糟糕:

只有第一次执行该方法的时候synchronized才起作用, 一旦我们设定好了成员变量那个引用到具体的实例, 以后就不需要synchronized这个方法了, 除了第一次, 以后这就是额外的开销.

还能改进多线程吗

1. 如果性能不是那么重要, 就继续使用synchronized吧. 但是要记住使用synchronized之后运行速度可能会差100倍(JVM).

2. 那就不如早点把实例给创建出来, 而不是懒创建.

例如:

使用静态的成员引用, 这样类在加载的时候就把实例创建出来了(保证在任何线程访问之前就会创建出来).

3. 使用"双重检查锁"来减少对sync的使用.

这就是首先检查实例是否被创建了, 如果没有那么进入sync块. 第一创建实例的时候时sync的, 在块里面, 再检查一次实例是否为null, 然后创建实例.

volatile关键字会保证被单体实例化的时候多线程会正确的处理uniqueInstance变量.

所以如果性能是问题, 就可以使用这个方法.

其他问题

Q: 如果我创建一个类, 里面都是静态方法和静态变量, 那么它的效果和单体模式不是一样的吗?

A: 是的, 如果你类没有其他依赖并且初始化并不复杂的话.

Q: 可以继承单体模式吗?

A: 简单的回答就是: No.

Q: 为什么单体模式比全局变量好?

A: 全局变量会污染命名空间, 当然了单体模式写不好也很烂.

总结

C# 实现

ChocolateBoiler:

namespace SingletonPattern
{
    public class ChocolateBoiler
    {
        public bool Empty { get; private set; }
        public bool Boiled { get; private set; }

        private static ChocolateBoiler _uniqueInstance;

        private ChocolateBoiler()
        {
            Empty = true;
            Boiled = false;
        }

        public static ChocolateBoiler GetInstance()
        {
            return _uniqueInstance ?? (_uniqueInstance = new ChocolateBoiler());
        }

        public void Fill()
        {
            if (Empty)
            {
                Empty = false;
                Boiled = false;
            }
        }

        public void Drain()
        {
            if (!Empty && Boiled)
            {
                Empty = true;
            }
        }

        public void Boil()
        {
            if (!Empty && !Boiled)
            {
                Boiled = true;
            }
        }
    }
}

 

SynchronizedChocolateBoiler:

using System.Runtime.CompilerServices;

namespace SingletonPattern
{
    public class SynchronizedChocolateBoiler
    {
        public bool Empty { get; private set; }
        public bool Boiled { get; private set; }

        private static SynchronizedChocolateBoiler _uniqueInstance;

        private SynchronizedChocolateBoiler()
        {
            Empty = true;
            Boiled = false;
        }

        [MethodImpl(MethodImplOptions.Synchronized)]
        public static SynchronizedChocolateBoiler GetInstance()
        {
            return _uniqueInstance ?? (_uniqueInstance = new SynchronizedChocolateBoiler());
        }

        public void Fill()
        {
            if (Empty)
            {
                Empty = false;
                Boiled = false;
            }
        }

        public void Drain()
        {
            if (!Empty && Boiled)
            {
                Empty = true;
            }
        }

        public void Boil()
        {
            if (!Empty && !Boiled)
            {
                Boiled = true;
            }
        }
    }
}

 

DoubleCheckChocolateBoiler:

namespace SingletonPattern
{
    public class DoubleCheckChocolateBoiler
    {
        public bool Empty { get; private set; }
        public bool Boiled { get; private set; }

        private static volatile DoubleCheckChocolateBoiler _uniqueInstance;
        private static readonly object LockHelper = new object();

        private DoubleCheckChocolateBoiler()
        {
            Empty = true;
            Boiled = false;
        }

        public static DoubleCheckChocolateBoiler GetInstance()
        {
            if (_uniqueInstance == null)
            {
                lock (LockHelper)
                {
                    if (_uniqueInstance == null)
                    {
                        _uniqueInstance = new DoubleCheckChocolateBoiler();
                    }
                }
            }
            return _uniqueInstance;
        }

        public void Fill()
        {
            if (Empty)
            {
                Empty = false;
                Boiled = false;
            }
        }

        public void Drain()
        {
            if (!Empty && Boiled)
            {
                Empty = true;
            }
        }

        public void Boil()
        {
            if (!Empty && !Boiled)
            {
                Boiled = true;
            }
        }
    }
}

由于这里面提到了多线程, 所以我会另写一篇关于C#/.NET Core异步和多线程的文章(也会是书上的内容, 这本书叫 C# 7 in a Nutshell, 我认为这是最好的C#/.NET Core参考书, 可是没有中文的, 所以我就是做一下翻译和精简)....

这个系列的代码我放在这里了: https://github.com/solenovex/Head-First-Design-Patterns-in-CSharp

下面是我的关于ASP.NET Core Web API相关技术的公众号--草根专栏:

目录
相关文章
|
10天前
|
数据可视化 网络协议 C#
C#/.NET/.NET Core优秀项目和框架2024年3月简报
公众号每月定期推广和分享的C#/.NET/.NET Core优秀项目和框架(每周至少会推荐两个优秀的项目和框架当然节假日除外),公众号推文中有项目和框架的介绍、功能特点、使用方式以及部分功能截图等(打不开或者打开GitHub很慢的同学可以优先查看公众号推文,文末一定会附带项目和框架源码地址)。注意:排名不分先后,都是十分优秀的开源项目和框架,每周定期更新分享(欢迎关注公众号:追逐时光者,第一时间获取每周精选分享资讯🔔)。
|
1月前
|
SQL 数据库 C#
C# .NET面试系列十一:数据库SQL查询(附建表语句)
#### 第1题 用一条 SQL 语句 查询出每门课都大于80 分的学生姓名 建表语句: ```sql create table tableA ( name varchar(10), kecheng varchar(10), fenshu int(11) ) DEFAULT CHARSET = 'utf8'; ``` 插入数据 ```sql insert into tableA values ('张三', '语文', 81); insert into tableA values ('张三', '数学', 75); insert into tableA values ('李四',
61 2
C# .NET面试系列十一:数据库SQL查询(附建表语句)
|
1月前
|
开发框架 算法 搜索推荐
C# .NET面试系列九:常见的算法
#### 1. 求质数 ```c# // 判断一个数是否为质数的方法 public static bool IsPrime(int number) { if (number < 2) { return false; } for (int i = 2; i <= Math.Sqrt(number); i++) { if (number % i == 0) { return false; } } return true; } class Progr
58 1
|
1月前
|
并行计算 安全 Java
C# .NET面试系列四:多线程
<h2>多线程 #### 1. 根据线程安全的相关知识,分析以下代码,当调用 test 方法时 i > 10 时是否会引起死锁? 并简要说明理由。 ```c# public void test(int i) { lock(this) { if (i > 10) { i--; test(i); } } } ``` 在给定的代码中,不会发生死锁。死锁通常是由于两个或多个线程互相等待对方释放锁而无法继续执行的情况。在这个代码中,只有一个线程持有锁,且没有其他线程参与,因此不
102 3
|
3天前
|
开发框架 前端开发 JavaScript
采用C#.Net +JavaScript 开发的云LIS系统源码 二级医院应用案例有演示
技术架构:Asp.NET CORE 3.1 MVC + SQLserver + Redis等 开发语言:C# 6.0、JavaScript 前端框架:JQuery、EasyUI、Bootstrap 后端框架:MVC、SQLSugar等 数 据 库:SQLserver 2012
|
4天前
|
设计模式 存储 Java
Java设计模式:解释一下单例模式(Singleton Pattern)。
`Singleton Pattern`是Java中的创建型设计模式,确保类只有一个实例并提供全局访问点。它通过私有化构造函数,用静态方法返回唯一的实例。类内静态变量存储此实例,对外仅通过静态方法访问。
12 1
|
1月前
|
SQL 存储 关系型数据库
C# .NET面试系列十:数据库概念知识
#### 1. 为什么要一定要设置主键? 设置主键是数据库设计中的一个重要概念,有几个主要原因: 1、唯一性 ```c# 主键必须保证表中的每一行都有唯一的标识。这样可以避免数据冗余和不一致性。如果没有主键或者主键不唯一,就可能出现数据混乱或错误。 ``` 2、查询性能 ```c# 数据库系统通常会使用主键来加速数据检索。主键通常会被索引,这样可以更快速地找到特定行的数据,提高查询效率。 ``` 3、关联性 ```c# 主键常常用于建立表与表之间的关系。在关系数据库中,一个表的主键通常与其他表中的外键建立关联,这种关系对于数据的一致性和完整性非常重要。 ``` 4、数据完
131 1
C# .NET面试系列十:数据库概念知识
|
1月前
|
XML 开发框架 .NET
C# .NET面试系列八:ADO.NET、XML、HTTP、AJAX、WebService
## 第二部分:ADO.NET、XML、HTTP、AJAX、WebService #### 1. .NET 和 C# 有什么区别? .NET(通用语言运行时): ```c# 定义:.NET 是一个软件开发框架,提供了一个通用的运行时环境,用于在不同的编程语言中执行代码。 作用:它为多语言支持提供了一个统一的平台,允许不同的语言共享类库和其他资源。.NET 包括 Common Language Runtime (CLR)、基础类库(BCL)和其他工具。 ``` C#(C Sharp): ```c# 定义: C# 是一种由微软设计的面向对象的编程语言,专门为.NET 平台开发而创建。 作
173 2
|
1月前
|
开发框架 中间件 .NET
C# .NET面试系列七:ASP.NET Core
## 第一部分:ASP.NET Core #### 1. 如何在 controller 中注入 service? 在.NET中,在ASP.NET Core应用程序中的Controller中注入服务通常使用<u>依赖注入(Dependency Injection)</u>来实现。以下是一些步骤,说明如何在Controller中注入服务: 1、创建服务 首先,确保你已经在应用程序中注册了服务。这通常在Startup.cs文件的ConfigureServices方法中完成。例如: ```c# services.AddScoped<IMyService, MyService>(); //
60 0
|
1月前
|
开发框架 前端开发 .NET
C# .NET面试系列六:ASP.NET MVC
<h2>ASP.NET MVC #### 1. MVC 中的 TempData\ViewBag\ViewData 区别? 在ASP.NET MVC中,TempData、ViewBag 和 ViewData 都是用于在控制器和视图之间传递数据的机制,但它们有一些区别。 <b>TempData:</b> 1、生命周期 ```c# TempData 的生命周期是短暂的,数据只在当前请求和下一次请求之间有效。一旦数据被读取,它就会被标记为已读,下一次请求时就会被清除。 ``` 2、用途 ```c# 主要用于在两个动作之间传递数据,例如在一个动作中设置 TempData,然后在重定向到另
95 5