单例模式
一个类只生成一个对象(确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例)
通过定义一个private的构造函数,避免被其他类new出来一个对象,不可以被实例化。
1.单例模式的懒加载 (第一次运行到此处,加载到内存)
class Singleton
{
private:
static Singleton* mySingleton;
public:
static A* getSingleton()
{
if(mySingleton != null)
{
return mySingleton;
}
}
};
static Singleton* mySingleton::mySingleton = mySingleton();
2.单例模式的快加载 (还没有使用就加载)
class SingletonB
{
private:
SingletonB();
static SingletonB* mySingleton;
Static pthread_mutex mutex;
public:
static SingletonB* getSingleton()
{
mutex_lock();
if(mySingleton == null)
{
return mySingleton;
}
mutex_unlock();
}
return mySingleton;
};
SingletonB* mySingletonB::mySingleton = null;
pthread_mutex_t SingletonB::mutex;
之前面试被问到一个问题,为什么要使用单例模式而不使用全局变量呢?两者又有什么区别呢?
当时想到的不是很多,于是后期整理了一下
单例模式和全局变量区别:
- 全局变量可以创建多个实例,但单例模式只能创建一个(每次通过属性Instance得到实例)
- 全局对象创建之后在栈上保存,但单例模式哪里第一次用到就在哪里创建
- 全局变量在项目中易引起变量名冲突,需要增加成本去维护,增加函数和模块之间的耦合度
- 当多个线程访问全局变量,需要使用同步机制进行保护这些变量
单例模式的优点
1)由於单例模式在内存中只有一个实例,减少了内存开支,特别是一个对象需要频繁地创建、销毁时,而且创建或销毁时性能又无法优化。
2)由于只生成一个实例,减少了系统的性能开销,当一个对象的产生需要比较多的资源时,如读取配置、产生其他依赖对象时,则可以通过在应用启动时直接产生一个单例对象,然后用永久驻留内存的方式解决。
3)可以避免对资源的多重占用,例如一个写文件动作,由于只有一个实例存在中,避免对同一个资源文件的同时写操作。
4)单例模式可以在系统设置全局的访问点,优化和共享资源访问。例如可以设计一个单例类,负责所有数据表的映射处理。
单例模式的缺点:
1)单例模式一般没有接口,扩展很困难。因为单例模式要求”自行实例化”,并且提供单一实例、接口或抽象类是不可能被实例化。特殊情况下可以实现接口、被继承等,需要在系统开发中根据环境判断。
2)单例模式对测试是不利的。在并行环境中,如果单例模式没有完成,是不能进行测试的,没有接口也不是使用mock的方式虚拟一个对象。
3)单例模式与单一职责原则有冲突。一个类应该只实现一个逻辑,而不关心他是否是单例模式,是不是单例取决于环境。
使用场景
在一个系统中,要求一个类有且仅有一个对象,如果出现多个对象就会出现“不良反应”,
具体场景如下:
1)要求生成唯一序列号的环境;
2)在整个项目中需要一个共享访问点或共享数据,例如一个web页面上的计数器,可以不用把每次刷新都记录到数据库中,使用单例模式保存计数器的值,并确保是线程安全的。
3)创建一个对象需要消耗的资源过多,如要访问IO和数据库等资源;
4)需要定义大量的静态常量和静态方法的环境,可以是单例模式(或者声明为static)
注意事项:
在高并发情况下,需要注意单例模式的线程同步问题。
实现Singleton模式
如果让我们设计一个类,只能生成该类的一个实例,我们需要把构造函数设为私有函数以禁止他人创
建实例。可以定义一个静态的实例,在需要的时候创建该实例。
public sealed class Singleton1
{
private Singleton1()
{
}
private static Singleton1 instance = null;
public static Singleton1 Instance
{
get
{
if ( instance == null )
instance = new Singleton1();
return instance;
}
}
}
此次代码实现在Singleton1的静态属性Instance中,只有在instance为null时才创建一个实例,以避免重复创建。同时把构造函数定义为私有函数,可以确保只创建一个实例。
但是,这只适用於单线程环境,在多线程的情况下就会产生问题。设想如果两个线程同时运行到判断instance是否为null的if条件语句,并且此时instance没有创建时,那么两个线程都会创建一个实例,此时Singleton1就不再满足单例模式的要求了。
在《剑指offer》中,看到了可以在多线程下进行工作的单例模式。
为了保证在多线程下只能得到类型的一个实例,需要加上一个同步锁。
public sealed class Singleton2
{
private Singleton2()
{
}
private static readonly object syncObj = new object();
private static Singleton2 instance = null;
public static Singleton2 Instance
{
get
{
lock ( syncObj )
{
if ( instance == null )
instance = new Singleton2();
}
return instance;
}
}
}
我们还是假设有两个线程同时向创建一个实例。由于在一个时刻只有一个线程能得到同步锁,当第
一个线程加上锁,第二个线程只能等待。当第一个线程发现实例还没有创建时,会创建一个实例。接
着第一个线程释放同步锁,此时第二个线程可以加上同步锁并运行接下来的代码。
这时候由于实例已经被第一个线程创建出来,第二个线程就不会重复创建实例,保证了多线程下只得
到一个实例。
但是,每次通过属性Instance得到实例,都会试图加上一个同步锁,而加锁是一个非常耗时的操作,在没有必要的时候我们应该去避免加锁。
可行的解法:加同步锁前后两次判断实例是否存在
我们只是在实例没有创建之前需要加锁操作,以保证只有一个线程创建出实例。而当实例已经创建之后,我们已经不需要再做枷锁操作了。
public sealed class Singleton3
{
private Singleton3()
{
}
private static object syncObj = new object();
private static Singleton3 instance = null;
public static Single3 Instance
{
get
{
if ( instance == null )
{
lock (instance == null )
{
if (instance == null )
instance = new Singleton3();
}
}
return instance;
}
}
}
Singleton3只有当instance为null即没有创建时,需要加锁操作。当instance已经创建,则无需进行加锁。这样下来相对于Singleton2,效率好很多。
还有一种解法,利用静态构造函数
这里利用到了C#种一个语法特性(PS;本人对C#不是特别了解,也是从书中学到的一种)。
C#语法中有一个函数能确保只调用依次,那就是静态构造函数。
实现如下:
public sealed class Singleton4
{
private Singleton4()
{
}
private static Singleton4 instance = new Singleton4();
public static Singleton4 Instance
{
get
{
return instance;
}
}
}
由于C#是在调用静态构造函数时初始化静态变量,.NET运行时能够确保只调用依次静态构造函数,保证了只初始化一次instance。.NET 运行时发现第一次使用一个类型时会自动调用该类型的静态构造函数。因此,在Singleton4中,实例instance会在第一次用到Singleton4时会被创建。
但是,这个实现单例模式方式,仍然会过早地创建实例,从而降低内存的使用效率。
接下来还有非常不错的解法:实现按需创建实例
public sealed class Singleton5
{
Singleton5()
{
}
public static Single Singleton5 Instance
{
get
{
return Nested.instance;
}
}
class Nested /*类默认私有*/
{
static Nested()
{
}
internal static readonly Singleton5 instance = new Singleton5();
}
}
在Singleton5中,在内部定义一个私有类型Nested。当第一次用到这个嵌套类型时,会调用静态构造函数创建Singleton5的实例instance。
类型Nested只在属性Singleton5.Instance中被用到,由于其私有属性他人无法使用Nested类型。因此当我们第一次试图通过属性Singleton5.Instance得到Singleton5的实例时,会自动调用Nested的静态构造函数创建实例instance。
当然,如果不调用属性Singleton5.Instance,也就不会触发.NET运行时调用Nested,也不会创建实例,真正做到了按需创建。
上述提到的这些方法中,
- Singleton1在多线程环境中不能正常工作;
- Singleton2通过加同步锁,虽然能在多线程下正常工作但时间效率很低;
- Singleton3通过加同步锁前后两次判断实例是否存在,确保只创建一个实例;
- Singleton4利用了C#的静态构造函数的特性,确保只创建一个实例;
- Singleton5利用私有嵌套类型的特性,做到只有在真正需要时才会创建,提高空间使用效率。
参考资料:
《设计模式之禅》
《GoF23种设计模式解析》
《剑指offer》