单例模式的介绍与实现
介绍单例模式之前我们先来介绍下什么是设计模式,所谓设计模式简单来说就是根据开发者先辈们的经验而总结出的解决问题的方式,可以说是前人经验和心血的体现。
有了设计模式之后,我们可以少走很多弯路,利用设计模式来轻松解决对应的问题。
话不多说,今天先来介绍最容易入门和掌握的设计模式——单例模式
单例模式:我们知道,在oo语言中,例如c#, JAVA,我们想要创建对象只要通过关键字new 即可,那么如果我们希望整个程序运行过程中某个类只有一个实例该怎么实现?
这个时候单例模式就出现了,它的目的就是确保程序运行过程中某个类只能有一个实例,并且提供一个全局访问点。
随着开发语言的演变,开发环境以及需求的多样性,单例模式也有了众多的实现方式,接下来会简单的介绍几个。
1. 经典实现
通过将构造函数设置为私有,以及暴露出一个静态方法,来实现单例模式。
public class SingletonPattern { private SingletonPattern(){} private static SingletonPattern uniqueInstance; public static SingletonPattern getInstance() { if (uniqueInstance == null) { uniqueInstance = new SingletonPattern(); } return uniqueInstance; } }
var test = SingletonPattern.getInstance(); var test2 = SingletonPattern.getInstance(); var test3 = SingletonPattern.getInstance(); Console.WriteLine(test == test2); Console.WriteLine(test == test3); Console.WriteLine(test2 == test3); var item1 = new NormalClass(); var item2 = new NormalClass(); Console.WriteLine(item1 == item2);
由上述代码可见,通过静态方法来返回唯一的实例,并且只有在初次调用这个静态方法的时候才实例化(延迟实例化),这样也就确保了再需要的时候实例化而不是在程序运行开始实例化。
那么上述代码是否已经达到我们的目的了呢?答案是不完全能够,如果程序运行中确保不会有多个线程同时访问这个静态方法,那么这个写法已经足够了,但是如果有呢?
public class SingletonPattern { private SingletonPattern(){} private static SingletonPattern uniqueInstance; public static SingletonPattern getInstance() { if (uniqueInstance == null) { Console.WriteLine("实例化");//标记实例化 uniqueInstance = new SingletonPattern(); } return uniqueInstance; } }
TaskFactory taskFactory = new TaskFactory(); for (var i = 0; i <= 5; i++) { taskFactory.StartNew(() => SingletonPattern.getInstance()); }
可以看到多线程下,该类被实例化了四次,那么该如何解决多线程的问题呢?
2. c#中通过lock关键字来解决线程同步问题
只要在上述代码中略微做些改动,就能够解决多线程的隐患
public class SingletonPattern { private SingletonPattern(){} private static SingletonPattern uniqueInstance; private static object locker = new object();//用于作为lock的对象,一个标志位,lock对象必须是引用类型 public static SingletonPattern getInstance() { lock (locker)//加锁,locker对象,以及代码块中的代码都不能被其他线程调用 { if (uniqueInstance == null) { Console.WriteLine("实例化");//标记实例化 uniqueInstance = new SingletonPattern(); } return uniqueInstance; } } }
加了锁之后,我们确实解决了线程同步的问题,但是加锁对性能有着影响,而且仔细观察上述代码,我们对每一个线程都进行了加锁,是否有这个必要呢?
其实我们加锁的目的就是为了确保只有一个线程来实例化了该类,一旦初始化之后,之后无论有几个线程同时调用这个静态方法,那么它们都将获得同一个对象。
3. 双重锁 if+lock
我们在上述代码中,做如下改动,确保只有在第一次实例化的时候,对线程加锁。
public class SingletonPattern { private SingletonPattern(){} private static SingletonPattern uniqueInstance; private static object locker = new object();//用于作为lock的对象,一个标志位,lock对象必须是引用类型 public static SingletonPattern getInstance() { if (uniqueInstance == null)//判断是否已经被实例化,如果没有,往下走 { lock (locker)//因为没有被实例化,所以加锁,接着实例化 { if (uniqueInstance == null) { Console.WriteLine("实例化");//标记实例化 uniqueInstance = new SingletonPattern(); } } } return uniqueInstance; } }
上述代码执行过程为先判断是否已经实例化,没有那么加锁创建实例,如果有则返回,一旦创建之后,就没有必要在去加锁。
这样既保证了多线程安全,又没有对性能造成过多的影响。
4. 除了上述几种方式之外,还可以通过把实例赋值给静态变量,那么也能确保该实例唯一,但是这个实例会在程序运行的时候就被初始化,而不是按需初始化。
实现方式不再这边描述,可以自行尝试。