一、什么是单例模式
保证一个类仅有一个实例,并提供了一个访问他的全局访问点。其目的是保证整个应用中只存在类的唯一实例。
比如我们在系统启动时,需要加载一些公共的配置信息,对整个应用程序的整个生命周期中都可见且唯一,这时需要设计成单例模式。如:Spring容器,session工厂,缓存,数据库连接池等等。
二、单例的实现主要是通过以下两个步骤:
1,将该类的构造方法定义为私有方法,这样其他处的代码就无法通过调用该类的构造方法来实例化该类的对象,只有通过该类提供的静态方法来得到该类的唯一实例;
2,在该类内提供一个静态方法,当我们调用这个方法时,如果类持有的引用不为空就返回这个引用,如果类保持的引用为空就创建该类的实例并将实例的引用赋予该类保持的引。
三、适用场景
1,需要生成唯一序列的环境
2,需要频繁实例化然后销毁代码的对象
3,有状态的工具类对象
4,频繁访问数据库或文件的对象
以下都是单例模式的经典实用场景
1,资源共享的情况下,避免由于资源操作时导致的性能活损耗。如日志文件,应用配置
2,控制资源的情况下,方便资源之间的相互通信。如线程池等。
应用场景举例:
1,外部资源:每台计算机有若干个打印机,但是只能有一个PrinterSpooler,以避免两个打印作业同时输出到打印机。
内部资源:大多数软件都有一个或者多个属性文件存放系统配置,这样的系统应该有一个对象管理这些属性文件
2,Windows的Task Manager(任务管理器)就是很典型的单例模式,我们不能同事打开两个windows task manager
3,windows的回收站也是典型的单例应用,回收站一直维护着仅有的一个实例
4,网站的计数器,一般也是采用单例模式来实现,否则难同步
5,应用程序的日志应用,一般都用单例模式,由于共享的日志文件一直处于打开状态,因为只能有一个实例去操作,否则内容不好追加
6,Web应用的配置对象的读取,一般也应用单例模式,这个是由于配置文件是共享资源。
7,数据库连接池的设计一般也是采用单例模式,因为数据库连接是一种数据库资源。数据库软件系统中使用数据库连接池,主要是节省打开或者关闭数据库连接索引引起的效率损耗,这种效率上的消耗是非常昂贵的,因为用单例模式来维护,就可以大大降低这种损耗。
8,多线程的线程池的设计一般也是采用单例模式,这是由于线程池要方便对池中的线程进行控制
9,操作系统的文件系统,也是大的单例模式实现的具体例子,一个操作系统只能有一个文件系统。
10,HttpApplication也是单例的典型应用。熟悉ASP.Net(IIS)的整个请求生命周期的人应该知道,HttpApplication也是单例模式,所有的HttpModule都共享一个HttpApplication实例。
四、如何保证实例的唯一
1,防止外部初始化
2,由类本身进行实例化
3,保证实例化一次
4,对外提供获取实例的对象
5,线程返券
五、单例模式的优缺点
优点:
内存中只有一个对象,节省内存空间
避免频繁的创建销毁对象,可以提高性能
避免对资源的多重占用,简化访问
为整个系统提供一个全局访问点
缺点:
不适用于变化频繁的对象;
滥用单利将带来一些问题,如为了节省资源将数据库连接池对象设计为的单利类,可能会导致共享连接池对象的程序过多而出现连接池溢出;
如果实例化的对象长时间不被利用,系统会认为该对象是垃圾而被回收,这可能会导致对象状态的丢失;
六、单例模式的实现
1,饿汉式
// 饿汉式单例
public class Singleton1 {
// 指向自己实例的私有静态引用,主动创建
private static Singleton1 singleton1 = new Singleton1();
// 私有的构造方法
private Singleton1(){}
// 以自己实例为返回值的静态的公有方法,静态工厂方法
public static Singleton1 getSingleton1(){
return singleton1;
}
}
我们知道,类加载的方式是需加载,切加载一次。。因此,在上述单例被加载时,就会实例化一个对象并交给自己的引用,供系统使用;而且,由于来在整个生命周期中只会被加载一次,因此只会创建一个实例,即能够充分保证单例。
优点:这种写法比较简单,就是在类装载的时候就完成实例化。避免了线程同步问题。
缺点:在类装载的时候就完成实例化,没有达到Lazy Loading的效果。如果哟偶的类从始至终都未使用过这个实例,则会造成内存浪费。
2,懒汉式
// 懒汉式单例
public class Singleton2 {
// 指向自己实例的私有静态引用
private static Singleton2 singleton2;
// 私有的构造方法
private Singleton2(){}
// 以自己实例为返回值的静态的公有方法,静态工厂方法
public static Singleton2 getSingleton2(){
// 被动创建,在真正需要使用时才去创建
if (singleton2 == null) {
singleton2 = new Singleton2();
}
return singleton2;
}
}
我们从懒汉式单例可以看到,单例实例被延迟加载,即只有在真正使用的时候才会实例化一个对象并交给自己的引用。
这种写法起到了Lazy Loading的效果,但是只能在单线程下使用。如果多线程下,一个线程进入了if(singletion==null)判断语句块,还未来得及往下执行,另一个线程也通过了这个判断语句,这时变回产生多个实例。所以在多线程下不可用这种方式。
3,接下来对它进行线程安全改造
(1)同步锁
public synchronized static Singleton getInstance() {
if (singleton == null) {
singleton = new Singleton();
}
return singleton;
}
优点:线程安全
缺点:每次获取实例都要枷锁,耗费资源,其实主要实例已经生成,以后获取就不需要在锁了。
(2)双重检查锁
public class Singleton
{
private static Singleton instance;
//程序运行时创建一个静态只读的进程辅助对象
private static readonly object syncRoot = new object();
private Singleton() { }
public static Singleton GetInstance()
{ //先判断是否存在,不存在再加锁处理
if (instance == null)
{
//在同一个时刻加了锁的那部分程序只有一个线程可以进入
lock (syncRoot)
{
if (instance == null)
{
instance = new Singleton();
}
}
}
return instance;
}
}
public static Singleton getInstance() {
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
代码中我们进行了两次if(singletion==null)检查,这样就可以保证线程安全了。这样,实例化代码只执行一次,后面再次访问时,判断if(singletion==null)直接return实例化对象。
使用了双重检测同步延迟加载去创建单例的做法是一个非常优秀的做法,其不但保证了单例,而且切实提高了程序运行效率
优点:线程安全;延迟加载;效率较高。
(3)静态内部类
public class Singleton {
private Singleton() {
}
private static class SingletonHolder {
private static final Singleton singleton = new Singleton();
}
public static Singleton getInstance() {
return SingletonHolder.singleton;
}
}
优点:既避免了同步带来的性能损耗,又能够延迟加载