Singleton Pattern

单例模式——

单例模式是六中23种设计模式中最简单的一种,虽然如此单例模式还是有值得探讨的地方。

单例模式的用处

顾名思义,单例模式就是想产生一个唯一的实例,而这样做的目的在于:

  • 对于某些频繁使用的全局对象,如果频繁地创建和销毁会占用很多的系统资源
  • 在一些场景中,需要唯一的实例,比如序列号、资源管理器等,其本身要就具有唯一性。

单例模式的简介

单例模式一般分为两类,俗称懒汉式和饿汉式——懒汉式是指当真正要使用到这个对象时才去创建这个对象;而饿汉式则是在程序运行之初就完成对象的创建。

要想实现单例模式,首先就要把构造函数私有化,这样外部就无法通过调用构造函数来生成更多的实例了。但是这时就要对外提供产生实例的接口,并在内部完成单例的创建。

单例模式的几种实现方式

  1. 最简单的单例模式可以采用一个静态字段来保存单例,外界调用创建单例的方法时就把这个对象返回。静态字段会在程序一开始就初始化(由CLR实现),因此这是饿汉式的单例模式。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    /// <summary>
    /// 最简单的单例模式,使用静态字段保存唯一单例,对外提供一个方法访问来获取单例
    /// </summary>
    public class Singleton
    {
    private static Singleton _singelton = new Singleton();
    private Singleton()
    {

    }
    public static Singleton CreatInstance()
    {
    return _singelton;
    }

    }
  1. 上面这种单例模式的实现方式存在一定的问题,试想如果这个单例在程序运行一开始并没有被使用,它就会占据系统的资源,会影响程序的效率(一个典型的例子是数据库链接,会占用大量资源)。所以改进的单例模式使用Lazy Loading,也就是懒汉模式。

    /// <summary>
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    /// <summary>
    /// 实现了Lazy Loading的单例模式,没有使用不会占用资源,但是它不是线程安全的
    /// </summary>
    public class Singleton2
    {
    private long sum = 0;
    private static Singleton2 _singelton = null;
    private Singleton2()
    {
    Console.WriteLine($"{this.GetType().Name} 被构造了");
    }
    public void Change()
    {
    sum++;

    }
    public static Singleton2 CreatInstance()
    {
    if (_singelton == null)
    {
    _singelton = new Singleton2();

    }
    return _singelton;
    }
    public static void Show()
    {
    Console.WriteLine(_singelton.sum);
    }
    }
  1. 上一种单例模式可以满足一般的需求,但当涉及到多线程并发时,上一种方法会尝试问题,因为它不是线程安全的,所以需要枷锁同步

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    /// <summary>
    /// 实现了Lazy Loading并且是线程安全的,但是在高并发的情况下会产生等待
    /// </summary>
    public class Singleton3
    {
    private long sum = 0;
    private static Singleton3 _singleton = null;
    private static readonly object locker = new object();
    private Singleton3()
    {
    Console.WriteLine($"{this.GetType().Name} 被构造了");
    }
    public void Change()
    {
    lock (locker)
    sum++;

    }
    public static Singleton3 CreatInstance()
    {

    lock (locker)
    {
    if (_singleton == null)
    {
    _singleton = new Singleton3();

    }
    }
    return _singleton;
    }
    public static void Show()
    {
    Console.WriteLine(_singleton.sum);
    }
    }
  1. 现在已经接近完美了,但是任存在一个问题——性能的消耗,因为每次调用CreateInstance方法时,都要争取锁而排队,会造成阻塞。所以我们希望当单例完成创建后不要再排队,这就有名为了Double-Checked的单例模式

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    /// <summary>
    /// 使用Double-check,与前一种方法一样,但是性能有提高,单例已生成的情况下不用等待
    /// </summary>
    public class Singleton4
    {

    private static Singleton4 _singelton = null;
    private static readonly object locker = new object();
    private Singleton4()
    {
    Console.WriteLine($"{this.GetType().Name} 被构造了");
    }
    public static Singleton4 CreatInstance()
    {
    if (_singelton == null)
    {
    lock (locker)
    {
    if (_singelton == null)
    {
    _singelton = new Singleton4();

    }
    }
    }
    return _singelton;
    }

    }
  1. 现在事情似乎已经很完美了,但是如果细究一下我们会发现这样一个问题:当多个线程访问CreateInstance时,(单例还没有创建的情况下)它们都会进入第一个if语句开始排队,会有一个线程争得锁进入第二个if,然后调用构造函数创建单例,创建完后释放锁,第二个线程进入,第二个线程首先会检查_singleton的状态是否为空,现在问题就出现了,如果这个描述对象是否为空的属性如果没有及时更新,那么就会导致多个实例的产生。这种情况是可能存在的,这是编译器所决定的,由于读写无序则会导致前面的情况出现(先访问了实例是否存在的属性,之后才进行了这个属性的写操作)。改进的方法是在存储单例的静态字段前加上valotile关键词。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    /// <summary>
    /// 也可以使用volatile关键字保证线程安全
    /// </summary>
    public class Singleton5
    {
    private static readonly object locker = new object();
    private static volatile Singleton5 _singelton = null;
    public static Singleton5 CreatInstance()
    {
    if (_singelton == null)
    {
    lock (locker)
    {
    if (_singelton == null)
    {
    _singelton = new Singleton5();
    Console.WriteLine("Singleton 被构造了");
    }
    }
    }
    return _singelton;
    }
    }

    注:volatile关键字的作用是保证被它修饰的对象(包括引用类型、指针类型、整型、具有整数基类型的枚举类型、泛型等)是和改变同步的,即永远是最新的(可以理解为volatile修饰的对象写操作先于读操作),具体原理涉及内存的操作,此处不做叙述。

关于单例线程安全问题的直观验证

根据上面封装的类,当我们运行如下代码时

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
static void Main(string[] args)
{
List<Task> tasks1 = new List<Task>();
for (int i = 0; i < 10000; i++)
{
Singleton2 singleton = Singleton2.CreatInstance();
tasks1.Add(Task.Run(() => singleton.Change()));
}
Task.WaitAll(tasks1.ToArray());
Singleton2.Show();
Console.WriteLine("***************************************************************");
List<Task> tasks2 = new List<Task>();
for (int i = 0; i < 10000; i++)
{
Singleton3 singleton = Singleton3.CreatInstance();
tasks2.Add(Task.Run(() => singleton.Change()));
}
Task.WaitAll(tasks2.ToArray());
Singleton3.Show();
Console.Read();
}

会得到如下的结果

mark

这样我们可以直观地看到线程安全问题确实存在。

文章目录
  1. 1. 单例模式——
    1. 1.0.1. 单例模式的用处
    2. 1.0.2. 单例模式的简介
    3. 1.0.3. 单例模式的几种实现方式
    4. 1.0.4. 关于单例线程安全问题的直观验证