您的位置:首页 > 其它

[置顶] 我的设计模式学习笔记------>单例模式(Singleton)

2013-10-17 11:24 316 查看

一、前言

有些时候,允许自由创建某个类的实例是没有意义,还可能造成系统性能下降(因为创建对象所带来的系统开销问题)。例如整个Windows系统只有一个窗口管理器,只有一个回收站等。在Java EE应用中可能只需要一个数据库引擎访问点,Hibernate访问时只需要一个SessionFactory实例,如果在系统中为它们创建多个实例就没有太大的意义。

如果一个类始终只能创建一个实例,则这个类被称为单例类,这种模式就被称为单例模式。

对Spring框架而言,可以在配置Bean实例时指定scope="singleton"类配置单例模式。不仅如此,如果配置<bean .../>元素时没有指定scope属性,则该Bean实例默认是单例的行为方式。

Spring推荐将所有业务逻辑组件、DAO组件、数据源组件等配置成单例的行为方式,因为这些组件无须保存任何用户状态,故所有客户端都可以共享这些业务逻辑组件、DAO组件,因此推荐奖这些组件配置成单例的行为方式。

二、基本定义

在阎宏博士的《JAVA与模式》一书中开头是这样描述单例模式的:

作为对象的创建模式,单例模式确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例,这个类称为单例类,它提供全局访问的方法。



单例模式有以下几个特点:



单例类只能有一个实例。

单例类必须自己创建自己的唯一实例。

单例类必须给所有其他对象提供这一实例。

单例模式的模式结构:



单例模式可以说是最简单的设计模式了,它仅有一个角色Singleton。



Singleton:单例。



三、单例模式的具体实现

1、饿汉式实现单例模式

public class EagerSingleton {
private EagerSingleton() {
}

private static EagerSingleton instance = new EagerSingleton();

public static EagerSingleton getInstance() {
return instance;
}
}

上面的例子中,在这个类被加载时,静态变量instance会被初始化,此时类的私有构造子会被调用。这时候,单例类的唯一实例就被创建出来了。

饿汉式其实是一种比较形象的称谓。既然饿,那么在创建对象实例的时候就比较着急,饿了嘛,于是在装载类的时候就创建对象实例。

饿汉式是典型的空间换时间,当类装载的时候就会创建类的实例,不管你用不用,先创建出来,然后每次调用的时候,就不需要再判断,节省了运行时间。

2、懒汉式实现单例模式

public class LazySingleton {
private LazySingleton() {
}

private static LazySingleton instance = null;

public static synchronized LazySingleton getInstance() {
if (instance==null) {
instance=new LazySingleton();
}
return instance;
}
}

上面的懒汉式单例类实现里对静态工厂方法使用了同步化,以处理多线程环境。
懒汉式其实是一种比较形象的称谓。既然懒,那么在创建对象实例的时候就不着急。会一直等到马上要使用对象实例的时候才会创建,懒人嘛,总是推脱不开的时候才会真正去执行工作,因此在装载对象的时候不创建对象实例。

懒汉式是典型的时间换空间,就是每次获取实例都会进行判断,看是否需要创建实例,浪费判断的时间。当然,如果一直没有人使用的话,那就不会创建实例,则节约内存空间。

由于懒汉式的实现是线程安全的,这样会降低整个访问的速度,而且每次都要判断。那么有没有更好的方式实现呢?

3、双重检查加锁实现单例模式

可以使用“双重检查加锁”的方式来实现,就可以既实现线程安全,又能够使性能不受很大的影响。那么什么是“双重检查加锁”机制呢?

所谓“双重检查加锁”机制,指的是:并不是每次进入getInstance方法都需要同步,而是先不同步,进入方法后,先检查实例是否存在,如果不存在才进行下面的同步块,这是第一重检查,进入同步块过后,再次检查实例是否存在,如果不存在,就在同步的情况下创建一个实例,这是第二重检查。这样一来,就只需要同步一次了,从而减少了多次在同步情况下进行判断所浪费的时间。

“双重检查加锁”机制的实现会使用关键字volatile,它的意思是:被volatile修饰的变量的值,将不会被本地线程缓存,所有对该变量的读写都是直接操作共享内存,从而确保多个线程能正确的处理该变量。

注意:在java1.4及以前版本中,很多JVM对于volatile关键字的实现的问题,会导致“双重检查加锁”的失败,因此“双重检查加锁”机制只只能用在java5及以上的版本。

public class DoubleCheckLockSingleton {
private DoubleCheckLockSingleton() {
}

/**
* 关键字volatile,它的意思是:被volatile修饰的变量的值,将不会被本地线程缓存,
* 所有对该变量的读写都是直接操作共享内存,从而确保多个线程能正确的处理该变量
*/
private static volatile DoubleCheckLockSingleton instance = null;

public static DoubleCheckLockSingleton getInstance() {
//先检查实例是否存在,如果不存在才进入下面的同步块
if (instance == null) {
//同步块,线程安全的创建实例
synchronized (DoubleCheckLockSingleton.class) {
//再次检查实例是否存在,如果不存在才真正的创建实例
if (instance == null) {
instance = new DoubleCheckLockSingleton();
}
}
}
return instance;
}
}

这种实现方式既可以实现线程安全地创建实例,而又不会对性能造成太大的影响。它只是第一次创建实例的时候同步,以后就不需要同步了,从而加快了运行速度。

由于volatile关键字可能会屏蔽掉虚拟机中一些必要的代码优化,所以运行效率并不是很高。因此一般建议,没有特别的需要,不要使用。也就是说,虽然可以使用“双重检查加锁”机制来实现线程安全的单例,但并不建议大量采用,可以根据情况来选用。

(更多关于双重检查锁定单例可以参考:我的Java开发学习之旅------>Java双重检查锁定及单例模式详解

根据上面的分析,常见的两种单例实现方式都存在小小的缺陷,那么有没有一种方案,既能实现延迟加载,又能实现线程安全呢?

4、静态内部类实现单例模式

public class InnerClassSingleton {
private InnerClassSingleton() {
}

/**
* 类级的内部类,也就是静态的成员式内部类,该内部类的实例与外部类的实例没有绑定关系,
*  而且只有被调用到时才会装载,从而实现了延迟加载。
*/
private static class InnerClassSingletonHolder {
/**
* 静态初始化器,由JVM来保证线程安全
*/
private static InnerClassSingleton instance = new InnerClassSingleton();
}

public static InnerClassSingleton getInstance() {
return InnerClassSingletonHolder.instance;
}
}


当getInstance方法第一次被调用的时候,它第一次读取InnerClassSingletonHolder.instance,导致InnerClassSingletonHolder类得到初始化;而这个类在装载并被初始化的时候,会初始化它的静态域,从而创建InnerClassSingleton的实例,由于是静态的域,因此只会在虚拟机装载类的时候初始化一次,并由虚拟机来保证它的线程安全性。

这个模式的优势在于,getInstance方法并没有被同步,并且只是执行一个域的访问,因此延迟初始化并没有增加任何访问成本。

5、枚举实现单例模式

public enum EmunSingleton {
/**
* 定义一个枚举的元素,它就代表了EmunSingleton的一个实例。
*/
INSTANCE;
}


使用枚举来实现单实例控制会更加简洁,而且无偿地提供了序列化机制,并由JVM从根本上提供保障,绝对防止多次实例化,
是更简洁、高效、安全的实现单例的方式。但是失去了类的一些特性,没有延迟加载。

四、单例模式的优缺点

1、优点

提供了对唯一实例的受控访问。因为单例类封装了它的唯一实例,所以它可以严格控制客户怎样以及何时访问它,并为设计及开发团队提供了共享的概念。

由于在系统内存中只存在一个对象,因此可以节约系统资源,对于一些需要频繁创建和销毁的对象,单例模式无疑可以提高系统的性能。

允许可变数目的实例。我们可以基于单例模式进行扩展,使用与单例控制相似的方法来获得指定个数的对象实例。

2、缺点

由于单例模式中没有抽象层,因此单例类的扩展有很大的困难。

单例类的职责过重,在一定程度上违背了“单一职责原则”。因为单例类既充当了工厂角色,提供了工厂方法,同时又充当了产品角色,包含一些业务方法,将产品的创建和产品的本身的功能融合到一起。

滥用单例将带来一些负面问题,如为了节省资源将数据库连接池对象设计为单例类,可能会导致共享连接池对象的程序过多而出现连接池溢出;现在很多面向对象语言(如Java、C#)的运行环境都提供了自动垃圾回收的技术,因此,如果实例化的对象长时间不被利用,系统会认为它是垃圾,会自动销毁并回收资源,下次利用时又将重新实例化,这将导致对象状态的丢失。

五、单例模式的使用场景

系统只需要一个实例对象,如系统要求提供一个唯一的序列号生成器,或者需要考虑资源消耗太大而只允许创建一个对象。

客户调用类的单个实例只允许使用一个公共访问点,除了该公共访问点,不能通过其他途径访问该实例。

在一个系统中要求一个类只有一个实例时才应当使用单例模式。反过来,如果一个类可以有几个实例共存,就需要对单例模式进行改进,使之成为多例模式。

==================================================================================================

作者:欧阳鹏 欢迎转载,与人分享是进步的源泉!

转载请保留原文地址:http://blog.csdn.net/ouyang_peng

==================================================================================================
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: