作为对象的创建模式[GOF95], 单例模式确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例。这个类称为单例类。 显然单例模式的要点有三个;一是某各类只能有一个实例;二是它必须自行创建这个实例;三是它必须自行向整个系统提供这个实例。
单例模式三种实现
java中单例模式是一种常见的设计模式,单例模式分三种:懒汉式单例、饿汉式单例、登记式单例三种。
1.饿汉式单例
package com;
/**
*
*
* 项目名称:singleton
* 类名称:EagerSingleton
* 类描述:饿汉单例模式
* 创建人:Alan
* 创建时间:2013-5-2 上午10:43:03
* 修改人:Alan
* 修改时间:2013-5-2 上午10:43:03
* 修改备注:
* @version
*
*/
public class EagerSingleton {
//在类加载时就实例化单例类的一个实例
private static EagerSingleton instance=new EagerSingleton();
/**
* 私有的默认构造子,保证外界无法直接实例化
*/
private EagerSingleton(){
}
/**
* 获得单例类对象的静态方法
* @return
*/
public static EagerSingleton getInstance(){
return instance;
}
}
Java 语言中单例类的一个最重要的特点是类的构造子是私有的,从而避免外界利用构造子直接创建出任意多的实例。值得指出的是,由于构造子是私有的,因此,此类不能被继承。
2.懒汉式单例
package com;
/**
*
*
* 项目名称:singleton
* 类名称:LazySingleton
* 类描述:懒汉单例模式非线程安全
* 创建人:Alan
* 创建时间:2013-5-2 上午10:54:17
* 修改人:Alan
* 修改时间:2013-5-2 上午10:54:17
* 修改备注:
* @version
*
*/
public class LazySingleton {
private static LazySingleton instance;
/**
* 私有的默认构造子,保证外界无法直接实例化
*/
private LazySingleton(){
}
/**
* 获得单例类对象的静态方法
* @return
*/
public static LazySingleton getInstance(){
if(instance==null){
instance=new LazySingleton();
}
return instance;
}
}
与饿汉式单例类相同之处是,类的构造子是私有的。与饿汉式单例类不同的是,懒汉式单例类在第一次被引用时将自己实例化。如果加载器是静态的,那么在懒汉式单例类被加载时不会将自己实例化。
但是以上实现没有考虑线程安全问题。所谓线程安全是指:如果你的代码所在的进程中有多个线程在同时运行,而这些线程可能会同时运行这段代码。如果每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,就是线程安全的。或者说:一个类或者程序所提供的接口对于线程来说是原子操作或者多个线程之间的切换不会导致该接口的执行结果存在二义性,也就是说我们不用考虑同步的问题。显然以上实现并不满足线程安全的要求,在并发环境下很可能出现多个Singleton实例。
package com;
/**
*
*
* 项目名称:singleton
* 类名称:LazySingleton
* 类描述:懒汉单例模式线程安全
* 创建人:Alan
* 创建时间:2013-5-2 上午10:54:17
* 修改人:Alan
* 修改时间:2013-5-2 上午10:54:17
* 修改备注:
* @version
*
*/
public class LazySingleton {
private static LazySingleton instance;
/**
* 私有的默认构造子,保证外界无法直接实例化
*/
private LazySingleton(){
}
/**
* 获得单例类对象的静态方法
* @return
*/
public static LazySingleton getInstance(){
//用双重检测法提高同步性能(只有instance为空时才同步)
if(instance==null){
synchronized (LazySingleton.class){
if(instance==null) {
instance=new LazySingleton();
}
}
}
return instance;
}
}
饿汉式单例类可以在Java 语言内实现, 但不易在C++ 内实现,因为静态初始化在C++ 里没有固定的顺序,因而静态的instance 变量的初始化与类的加载顺序没有保证,可能会出问题。这就是为什么GoF 在提出单例类的概念时,举的例子是懒汉式的。他们的书影响之大,以致Java 语言中单例类的例子也大多是懒汉式的。实际上,本书认为饿汉式单例类更符合Java 语言本身的特点。
3.登记式单例
登记式单例类是GoF 为了克服饿汉式单例类及懒汉式单例类均不可继承的缺点而设计的。
package com;
import java.util.HashMap;
/**
*
*
* 项目名称:singleton
* 类名称:RegSingleton
* 类描述:登记式单例父类
* 创建人:Alan
* 创建时间:2013-5-2 下午02:52:03
* 修改人:Alan
* 修改时间:2013-5-2 下午02:52:03
* 修改备注:
* @version
*
*/
public class RegSingleton {
//存放单例类实例的Map
private static HashMap registryMap = new HashMap();
/**
* 为了提高性能,这里采用饿汉式单例在类加载时就把单例类实例化
*/
static {
RegSingleton regSingleton=new RegSingleton();
registryMap.put(regSingleton.getClass().getName(), regSingleton);
}
/**
* 保护的默认构造函数(其他对象是无法new这个对象的,因为不是public的)
*/
protected RegSingleton(){
}
/**
* 获得单例类对象的静态方法
* @param name
* @return
*/
public static RegSingleton getInstance(String name){
//默认为父类单例类
if (name == null){
name = "com.RegSingleton";
}
//如果不存在则放入Map中(注册)
if (registryMap.get(name) == null){
try {
registryMap.put(name,Class.forName(name).newInstance()) ;
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
return (RegSingleton) (registryMap.get(name) );
}
}
package com;
/**
*
*
* 项目名称:singleton
* 类名称:RegSingletonChild
* 类描述:登记式单例子类
* 创建人:Alan
* 创建时间:2013-5-2 下午02:53:50
* 修改人:Alan
* 修改时间:2013-5-2 下午02:53:50
* 修改备注:
* @version
*
*/
public class RegSingletonChild extends RegSingleton {
//此处不能为private,否则登记式单例父类无法调用子类的构造函数而产生实例
protected RegSingletonChild() {
//这里默认调用了类类的构造函数super();
}
/**
* 获得单例类对象的静态方法
*/
static public RegSingletonChild getInstance()
{
return (RegSingletonChild)RegSingleton.getInstance("com.RegSingletonChild");
}
}
单例模式的优缺点
1.饿汉模式
优点
1.线程安全的
2.在类加载的同时已经创建好一个静态对象,调用时反应速度快。
缺点: 资源利用效率不高,可能getInstance永远不会执行到,但是执行了该类的其他静态方法或者加载了该类,那么这个实例仍然初始化了。
2.懒汉模式
优点: 资源利用率高,不执行getInstance就不会被实例,可以执行该类其他静态方法。
缺点: 第一次加载时发应不快 ,多线程使用不必要的同步开销大
单例模式的状态
1.有状态的单例类
一个单例类可以是有状态的(stateful),一个有状态的单例对象一般也是可变(mutable) 单例对象。
有状态的可变的单例对象常常当做状态库(repositary)使用。比如一个单例对象可以持有一个int 类型的属性,用来给一个系统提供一个数值惟一的序列号码,作为某个贩卖系统的账单号码。当然,一个单例类可以持有一个聚集,从而允许存储多个状态。
2.无状态的单例类
另一方面,单例类也可以是没有状态的(stateless),仅用做提供工具性函数的对象。既然是为了提供工具性函数,也就没有必要创建多个实例,因此使用单例模式很合适。一个没有状态的单例类也就是不变(Immutable) 单例类。
3.多个JVM 系统的分散式系统
EJB 容器有能力将一个EJB 的实例跨过几个JVM 调用。由于单例对象不是EJB,因此,单例类局限于某一个JVM 中。换言之,如果EJB 在跨过JVM 后仍然需要引用同一个单例类的话,这个单例类就会在数个JVM 中被实例化,造成多个单例对象的实例出现。一个J2EE应用系统可能分布在数个JVM 中,这时候不一定需要EJB 就能造成多个单例类的实例出现在不同JVM 中的情况。
如果这个单例类是没有状态的,那么就没有问题。因为没有状态的对象是没有区别的。但是如果这个单例类是有状态的,那么问题就来了。举例来说,如果一个单例对象可以持有一个int 类型的属性,用来给一个系统提供一个数值惟一的序列号码,作为某个贩卖系统的账单号码的话,用户会看到同一个号码出现好几次。在任何使用了EJB、RMI 和JINI 技术的分散式系统中,应当避免使用有状态的单例模式。
4.多个类加载器
同一个JVM 中会有多个类加载器,当两个类加载器同时加载同一个类时,会出现两个实例。在很多J2EE 服务器允许同一个服务器内有几个Servlet 引擎时,每一个引擎都有独立的类加载器,经有不同的类加载器加载的对象之间是绝缘的。比如一个J2EE 系统所在的J2EE 服务器中有两个Servlet 引擎:一个作为内网给公司的网站管理人员使用;另一个给公司的外部客户使用。两者共享同一个数据库,两个系统都需要调用同一个单例类。如果这个单例类是有状态的单例类的话,那么内网和外网用户看到的单例对象的状态就会不同。除非系统有协调机制,不然在这种情况下应当尽量避免使用有状态的单例类。
单例模式的具体应用
1.Spring中单例模式的运用
Spring中 默认所有的bean设置为单例模式,即对所有具有相同id的bean的请求,都返回一个共享bean实例,可以降低java创建和销毁时的系统开销。
单例模式在Spring 中其实是scope(作用范围)参数的缺省设定值。每个bean定义只生成一个对象实例,每次getBean请求获得的都是此实例。另一种和singleton对应的scope值---prototype多实例模式调用getBean时,就new一个新实例。
xml配置文件:
<bean id="dvdTypeDAO" class="com.machome.hibernate.impl.DvdTypeDAOImpl" />
测试代码:
ctx = new ClassPathXmlApplicationContext("spring-hibernate-mysql.xml");
DvdTypeDAO tDao1 = (DvdTypeDAO)ctx.getBean("dvdTypeDAO");
DvdTypeDAO tDao2 = (DvdTypeDAO)ctx.getBean("dvdTypeDAO");
运行:
true
com.machome.hibernate.impl.DvdTypeDAOImpl@15b0333
com.machome.hibernate.impl.DvdTypeDAOImpl@15b0333
说明前后两次getBean()获得的是同一实例,说明spring缺省是单例。
<bean id="dvdTypeDAO" class="com.machome.hibernate.impl.DvdTypeDAOImpl" scope="prototype" />
执行同样的测试代码
运行:
false
com.machome.hibernate.impl.DvdTypeDAOImpl@afae4a
com.machome.hibernate.impl.DvdTypeDAOImpl@1db9852
说明scope="prototype"后,每次getBean()的都是不同的新实例
由单例模式引出的知识拓展
1.反射可“破坏”单例模式
通过Constructor<?>[] getDeclaredConstructors() 这个方法获得所有构造器,然后可以得到私有的构造器,使用 cons. setAccessible(true) 突破私有构造。然后可以创建无限个单例实例。
package com;
import java.lang.reflect.Constructor;
/**
*
*
* 项目名称:singleton
* 类名称:ReverConstructor
* 类描述:通过反射构造单例对象
* 创建人:Alan
* 创建时间:2013-5-2 下午03:42:42
* 修改人:Alan
* 修改时间:2013-5-2 下午03:42:42
* 修改备注:
* @version
*
*/
public class ReverConstructor {
public static void main(String[] args) {
try {
Constructor<?>[] arry= Class.forName("com.EagerSingleton").getDeclaredConstructors();
for(int i=0;i<arry.length;i++){
//只拿非public的构造函数
if(!arry[i].isAccessible()){
arry[i].setAccessible(true);
//被反射的类要有不带参数的构造函数
EagerSingleton eagerSingleton=(EagerSingleton) arry[i].newInstance();
EagerSingleton eagerSingleton2=(EagerSingleton) arry[i].newInstance();
//可以构造出多个单例对象
System.out.println(eagerSingleton); //com.EagerSingleton@12b6651
System.out.println(eagerSingleton2); //com.EagerSingleton@4a5ab2
break;
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
2.防通过反射调用构造函数破坏单例模式创建多个实例
package com;
/**
*
*
* 项目名称:singleton
* 类名称:EagerSingleton
* 类描述:饿汉单例模式 防通过反射调用构造函数 创建多个实例
* 创建人:Alan
* 创建时间:2013-5-2 上午10:43:03
* 修改人:Alan
* 修改时间:2013-5-2 上午10:43:03
* 修改备注:
* @version
*
*/
public class EagerSingleton {
//在类加载时就实例化单例类的一个实例
private static EagerSingleton instance=new EagerSingleton();
//实例计数器
private static int indexCount = 1;
/**
* 私有的默认构造子,保证外界无法直接实例化
*/
private EagerSingleton(){
//对实例数进行判断,如果外界试图通过调用构造函数建立多个实例对象,则抛出异常(防通过反射调用构造函数)
indexCount++;
if(indexCount>2){
throw new RuntimeException("此类为单例类,不能创建多个对象!");
}
}
/**
* 获得单例类对象的静态方法
* @return
*/
public static EagerSingleton getInstance(){
return instance;
}
}
3.构造函数
使用构造器时需要记住:
<1>.构造器必须与类同名(如果一个源文件中有多个类,那么构造器必须与公共类同名)。
<2>.每个类可以有一个以上的构造器。
<3>.构造器可以有0个、1个或1个以上的参数。
<4>.构造器没有返回值。
<5>.构造器总是伴随着new操作一起调用。
<6>.子类默认都调用了父类的默认无参构造函数,如果父类没有默认的无参构造函数,则在子类的构造函数中必须手动调用父类的构造函数super(),且必需是子类的构造函数中的第一条语句。
4.访问权限控制
Java的访问权限控制修饰符,从最大权限到最小权限依次是:public、protected、包访问权限(默认,没有关键字)和private。对于类的访问权限只能是:public和包访问权限(但内部类可以是private或protected的);对于类中的成员的访问权限可以是上述的四种。下面是各种访问权限的作用。
作用域
| 当前类 | 同一package普通类 | 其他package普通类 | 同一package子孙类 | 其他package子孙类 |
public | √ | √ | √ | √ | √ |
protected | √ | √ | × | √ | √ |
默认 | √ | √ | × | √ | × |
private | √ | × | × | × | × |
5.访问权限控制的必要性
在《think in java》一书是写到:
若任何人都能使用一个类的所有成员,那么客户程序员可对那个类做任何事情,没有办法强制他们遵守任何约束。即便非常不愿客户程序员直接操作类内包含的一些成员,但倘若未进行访问控制,就没有办法阻止这一情况的发生——所有东西都会暴露无遗。
有两方面的原因促使我们控制对成员的访问。
第一个原因是防止程序员接触他们不该接触的东西——通常是内部数据类型的设计思想。若只是为了解决特定的问题,用户只需操作接口即可,毋需明白这些信息。我们向用户提供的实际是一种服务,因为他们很容易就可看出哪些对自己非常重要,以及哪些可忽略不计;
进行访问控制的第二个原因是允许库设计人员修改内部结构,不用担心它会对客户程序员造成什么影响。例如,我们最开始可能设计了一个形式简单的类,以便简化开发。以后又决定进行改写,使其更快地运行。若接口与实现方法早已隔离开,并分别受到保护,就可放心做到这一点,只要求用户重新链接一下即可。