什么是单例模式
对于一个软件系统的某些类而言,无需创建多个实例。如:Windows任务管理器,只能打开一个。确保一个类只有一个实例,而且自行实例化并向整个系统提供这个实例,这个类称为单例类,它提供全局访问的方法。线程池、缓存、对话框、处理偏好设置和注册表、日志对象、打印机和显卡等设备的驱动一般都是唯一实例即可。
定义:为了节约系统资源,有时需要确保系统中某个类只有唯一一个实例,当这个唯一实例创建成功后,我们无法再创建一个同类型的其它对象,所有的操作都只能基于这个唯一实例;为了确保对象的唯一性,我们可以通过单例模式来实现,就是单例模式的动机所在。单例模式为创建型模式。
创建型模式
对类的实例化进行了抽象,能够在软件中将对象的创建和使用进行分离;为了使软件结构更加清晰,外界对于这些对象只需要知道它们共同的接口,而不清楚具体的实现细节,使整个系统符合单一职现原则。
创建型模式在创建什么、由谁创建、何时创建等方面为设计者提供了尽可能大的灵活性,隐藏了类的实例的创建细节,通过隐藏对象如何被创建和组合在一起达到使整个系统独立的目的。
创建型模式的种类:
简单工厂模式、工厂方法模式、抽象工厂模式、建造者模式、原型模式、单例模式
单例模式的分类:
饿汉单例、懒汉单例。
饿汉单例示例(C#):
class TaskManager
{
//必须有与当前类相同类型的私有成员; 系统一运行就加载创建,由CLR创建,线程安全
private static TaskManager taskManager=new TaskManager();
//必须的私有构造函数,用户自定义了构造函数后,系统将不再提供默认构造函数
//private使用得无法在外部直接创建该类型的实例,但该类内部可以访问
//反之,系统提供的默认构造函数是public的,外部还是可以创建多个实例,就不符合创建唯一实例要求了
private TaskManager()
{
}
//必须有一个类的static方法返回这个唯一实例,第一次调用创建并返回实例,再次调用将直接返回实例
public static TaskManager GetInstance()
{
return taskManager;
}
}
懒汉单例示例(C#):
//懒汉单例,使用时才加载创建
class TaskManager
{
//必有有与当前类相同类型的私有成员
private TaskManager taskManager=null;
//必须的私有构造函数,用户自定义了构造函数后,系统将不再提供默认构造函数
//private使用得无法在外部直接创建该类型的实例,但该类内部可以访问
//反之,系统提供的默认构造函数是public的,外部还是可以创建多个实例,就不符合创建唯一实例要求了
private TaskManager()
{
}
//必须有一个类的static方法返回这个唯一实例,第一次调用创建并返回实例,再次调用将直接返回实例
public static TaskManager GetInstance()
{
//只创建一个
if(taskManager==null)
taskManager =new TaskManager(); //内部可以直接调用private的构造函数
return taskManager;
}
}
上述懒汉式单例在多线程访问时依然存在会创建非唯一实例(单例的初始化比较耗时的情况下会出现)问题,纠正后的代码如下:
class TaskManager
{
/// <summary>
/// 私有本类型成员
/// </summary>
private static TaskManager instance = null;
private static readonly object syncLock = new object(); //锁标志
/// <summary>
/// 私有构造函数,这样就无法从外部创建新的实例;如果不这样,系统将会提供默认的无参构造函数,但是public,这样就能从外部直接创建实例了,不符合单例规则
/// </summary>
private TaskManager()
{
Console.WriteLine("正在创建实例....");
System.Threading.Thread.Sleep(2000);
}
/// <summary>
/// 必提供外部获取实例的方法
/// </summary>
/// <returns></returns>
public static TaskManager GetInstance()
{
//双重检查,只让少部分线程通过此次判断,大部分线程访问到达此处时,实例化可能已经完成,就无需再进去了
if (instance == null)
{
lock (syncLock) //加锁,保证一次只有一个线程访问,但有性能损耗,每个线程都要等待
{
if (instance == null) //只有这一层,在构造函数初始化处理耗时较长时,多线程访问会出问题,可能会创建多个实例,
instance = new TaskManager(); //使用私有构造方法创建实例
}
}
return instance;
}
}
饿汉式单例类与懒汉式单例类比较
饿汉式单例类在类被加载时就将自己实例化,它的优点在于无须考虑多线程访问问题,可以确保实例的唯一性;从调用速度和反应时间角度来讲,由于单例对象一开始就创建了,因此要优于懒汉式单例。但无论系统在运行时是否需要使用该对象,类加载时都会创建,因此从资源利用效率角度来讲,饿汉式单例不及懒汉式单例,而且在系统加载时要创建单例对象,系统启动加载时间可能会较长。
懒汉式单例类在第一次使用时创建,无须一直占用系统资源,实现了延时加载,但是必须处理好多个线程间的访问题,特别是当单例类作为资源控制器,在实例化时必然涉及资源的初始化,而资源初始化很有可能耗费大量时间,这意味着出现多线程同时首次引用引类的机率变大,需要通过双重检查锁定等机制进行控制,就将导致系统性能受到一定影响。
解决饿汉式和懒汉式单例类缺点的一种方法 ,如下示例:
class TaskManager
{
private TaskManager()
{
}
//静态内部类,由C#运行时(CLR)创建实例
private static class Inner
{
public TaskManager taskManager=new TaskManager();
}
//必须有一个类的static方法返回这个唯一实例,第一次调用创建并返回实例,再次调用将直接返回实例
//在调用GetInstance方法时,静态内部类才加载,所以由CLR加载创建实例,线程安全
public static TaskManager GetInstance()
{
return Inner.taskManager; //调用静态内类中的实例,该实例由C#运行时创建,所以不需要担心多线程访问的问题
}
}
泛型单例的示例:
/// <summary>
/// 泛型单例
/// </summary>
/// <typeparam name="T"></typeparam>
class Single<T> where T : class,new()
{
//此句主要是为了防止创建泛型类的实例,取消此句会创建泛型类实例
protected Single()
{
}
/// <summary>
/// 静态内部类
/// </summary>
private static class Inner
{
/// <summary>
/// 私有本类型成员
/// </summary>
public static T instance = new T();
}
/// <summary>
/// 必提供外部获取实例的方法
/// </summary>
/// <returns></returns>
public static T GetInstance()
{
return Inner.instance;
}
}
class MySingle : Single<MySingle>
{
public MySingle()
{
Console.WriteLine("正在创建实例....MySingle");
System.Threading.Thread.Sleep(2000);
}
}
class TestSingle : Single<TestSingle>
{
public TestSingle()
{
Console.WriteLine("正在创建实例....TestSingle");
System.Threading.Thread.Sleep(2000);
}
}
调用:
class Program
{
static void Main(string[] args)
{
//多个线程情况下,双检查单例与只有锁的单例访问比较,双检查效率高
//System.Diagnostics.Stopwatch sw=new System.Diagnostics.Stopwatch();
//sw.Start();
//for(int i=0;i<10000;i++)
// Task.Run(() => TaskManager.GetInstance());
//sw.Stop();
// Console.WriteLine(sw.ElapsedMilliseconds);
//var ts = Task.Run(() => TaskManager.GetInstance());
//Console.WriteLine(0);
//var ts1 = Task.Run(() => TaskManager.GetInstance());
//Console.WriteLine(1);
//Console.WriteLine(ts.Result == ts1.Result);
//Console.WriteLine(2);
//Console.WriteLine(object.ReferenceEquals(ts.Result, ts1.Result));
// MySingle m1 = new MySingle();
// MySingle m2 = new MySingle();
Single<MySingle> m1 = Single<MySingle>.GetInstance();
Single<MySingle> m2 = Single<MySingle>.GetInstance();
//Single<MySingle> m3 = new Single<MySingle>();
Console.WriteLine(object.ReferenceEquals(m1, m2));
Console.Read();
}
}
似乎静态内部类看起来已经是最完美的方法了,其实不是,可能还存在反射攻击或者反序列化攻击。且看如下代码(Java):
public static void main(String[] args) throws Exception {
Singleton singleton = Singleton.getInstance();
Constructor<Singleton> constructor = Singleton.class.getDeclaredConstructor();
constructor.setAccessible(true);
Singleton newSingleton = constructor.newInstance();
System.out.println(singleton == newSingleton);
}
运行结果:
通过结果看,这两个实例不是同一个,这就违背了单例模式的原则了。
除了反射攻击之外,还可能存在反序列化攻击的情况。如下:
引入依赖:
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.8.1</version>
</dependency>
这个依赖提供了序列化和反序列化工具类。
Singleton类实现java.io.Serializable接口。
如下:
public class Singleton implements Serializable {
private static class SingletonHolder {
private static Singleton instance = new Singleton();
}
private Singleton() {
}
public static Singleton getInstance() {
return SingletonHolder.instance;
}
public static void main(String[] args) {
Singleton instance = Singleton.getInstance();
byte[] serialize = SerializationUtils.serialize(instance);
Singleton newInstance = SerializationUtils.deserialize(serialize);
System.out.println(instance == newInstance);
}
}
运行结果:
通过枚举实现单例模式
在effective java(这本书真的很棒)中说道,最佳的单例实现模式就是枚举模式。利用枚举的特性,让JVM来帮我们保证线程安全和单一实例的问题。除此之外,写法还特别简单。
public enum Singleton {
INSTANCE;
public void doSomething() {
System.out.println("doSomething");
}
}
调用方法:
public class Main {
public static void main(String[] args) {
Singleton.INSTANCE.doSomething();
}
}
直接通过Singleton.INSTANCE.doSomething()的方式调用即可。方便、简洁又安全。
单例模式优缺点:
优点:提供了唯一实例的受控访问,因为单例类封装了它的唯一实例,所以它可以严格控制客户怎样以及何时访问它。
由于系统内存中只存在一个对象,因此可以节约系统资源,对于一些需要频繁创建和销毁的对象单例模式无疑可以提高系统的性能。
允许可变数目的实例,基于单例模式我们可以进行扩展,使用与单例控制相似的方法来获得指定个数的对象实例,既节省系统资源,又解决了单例对象共享过多有损性能的问题。
缺点:单例模式没有抽象层,因此单例类的扩展有很大困难。
单例类职责过重,在一定程度上违背了“单一职责原则”,因为单例类既充当了工厂角色,提供了工厂方法,同时又充当了产品角色,包含一些业务方法,将产品的创建和产品的本身功能融合到一起。
由于很多语言的运行环境提供了自动垃圾回收的技术,因此,如果实例化的共享对象长时间不被利用,系统会认为它是垃圾,会自动销毁并回收资源,下次利用时又将重新实例化,这将导致共享的单例状态的丢失。
适用场景
系统只需要一个实例对象,如系统要求提供一个唯一的序列号生成器或资源管理器,或者需要考虑资源消耗太大而只允许创建一个对象。
客户调用类的单个实例只允许使用一个公共访问点,除了该公共访问点,不能通过其它途径访问该实例。