设计模式之单例模式Singleton

参考文献

参考1:http://www.iteye.com/topic/60179

参考2:研磨设计模式

参考3:http://cantellow.iteye.com/blog/838473

参考4:http://stackoverflow.com/questions/1673841/examples-of-gof-design-patterns (2011-11-16添加)

构造方法私有化(ps:2012-4-12)

类的封装性不只是体现在对属性的封装上,也可以体现在对方法的封装上。对方法的封装包含了对构造方法的封装。我们可以将构造方法的访问权限设定为private,代码实例如下:

View Code
public class Singleton {
    private Singleton()
    {
        
    }
    public void print()
    {
        System.out.println("Hello World!");
    }

}

如果我们要直接实例化一个Singleton对象,代码如下:

View Code
public class SingleDemo1 {
    
    public static void main(String[] args) {
        Singleton s1=new Singleton();
        s1.print();
    }
}

执行上述代码会出现如下错误:

The constructor Singleton() is not visible

这是因为构造方法是封装的,不能在类外部直接访问。

既然不能在类的外部外部调用构造函数进行实例化,那么就只能在类的内部进行实例化,代码如下:

View Code
public class Singleton {
    Singleton instance=new Singleton();//在类的内部生成实例化对象
    private Singleton()//对构造方法进行封装。
    {
        
    }
    public void print()
    {
        System.out.println("Hello World!");
    }

}

那么现在碰到的一个问题就是:如何将类内部的实例化对象instance传递到类的外部。这就涉及到static方面的知识。static类型的属性可以直接由类名称来直接访问,将instance声明为static类型,就可以直接通过类名称直接调用,代码如下:

View Code
public class Singleton {
    static Singleton instance=new Singleton();//在类的内部生成实例化对象
    private Singleton()//对构造方法进行封装。
    {
        
    }
    public void print()
    {
        System.out.println("Hello World!");
    }

}

在客户端中稍做修改进行实例化对象,代码如下:

View Code
public class Singleton {
    static Singleton instance=new Singleton();//在类的内部生成实例化对象
    private Singleton()//对构造方法进行封装。
    {
        
    }
    public void print()
    {
        System.out.println("Hello World!");
    }

}

上述程序的运行结果是:

Hello World!

仔细观察程序我们发现,上述程序虽然成功取得了Singleton的实例化对象并调用了其中的print()方法,但是考虑到类中的属性必须封装,所以这里要对instance属性进行封装,将其访问权限设定为private,封装属性之后通过方法获取这个instance。因为instance是静态属性,所以必须声明一个静态方法来访问这个静态属性,这个方法需要能够直接通过类名来调用,因此这个方法必须是静态方法。代码如下:

View Code
public class Singleton {
    private static Singleton instance=new Singleton();//在类的内部生成实例化对象
    private Singleton()//对构造方法进行封装。
    {
        
    }
    public static Singleton getInstance() {
        return instance;
    }
    public void print()
    {
        System.out.println("Hello World!");
    }
}

注意:静态成员与静态方法之间的关系:

  1. 静态成员可以被静态方法和非静态方法访问。
  2. 静态方法只能访问静态成员。

然后在客户端中稍作修改获取Singleton实例化对象,代码如下:

View Code
public class SingleDemo1 {
    
    public static void main(String[] args) {
        Singleton s1=Singleton.getInstance();//访问类中的静态方法获取对象实例。
        s1.print();
    }
}

要点:只要构造方法私有化,就可以控制实例化对象的产生。因此也产生了单例模式。下面介绍各种不同的单例模式,但是都是基于上述单例模式演变而来。

1.概要

单例模式适合于一个类只有一个实例的情况,比如窗口管理器,打印缓冲池和文件系统,还有读取配置文件,它们都是原型的例子。典型的情况是,那些对象的类型被遍及一个软件系统的不同对象访问,因此需要一个全局的访问指针,这便是众所周知的单例模式的应用。当然这只有在你确信你不再需要任何多于一个的实例的情况下。
单例模式的用意在于前一段中所关心的。通过单例模式你可以: 

  • 确保一个类只有一个实例被建立 
  • 提供了一个对对象的全局访问指针 
  • 在不影响单例类的客户端的情况下允许将来有多个实例 

2.单例模式的分类

在java中,单例模式的实现分为两种,一种称为懒汉式,一种称为饿汉式,其实就是在具体创建对象实例的处理上,有不同的实现方式。下面先来看看这两种实现方式的具体示例代码。

2.1懒汉式单例实现的示例(第一种单例模式)

View Code
/**
* 懒汉式单例实现的示例
*/
public class Singleton {
/**
* 定义一个变量来存储创建好的类实例
*/
private static Singleton uniqueInstance = null;
/**
* 私有化构造方法,好在内部控制创建实例的数目
*/
private Singleton(){
//
}
/**
* 定义一个方法来为客户端提供类实例
*
@return 一个Singleton的实例
*/
public static synchronized Singleton getInstance(){
//判断存储实例的变量是否有值
if(uniqueInstance == null){
//如果没有,就创建一个类实例,并把值赋值给存储类实例的变量
uniqueInstance = new Singleton();
}
//如果有值,那就直接使用
return uniqueInstance;
}
/**
* 示意方法,单例可以有自己的操作
*/
public void singletonOperation(){
//功能处理
}
/**
* 示意属性,单例可以有自己的属性
*/
private String singletonData;
/**
* 示意方法,让外部通过这些方法来访问属性的值
*
@return 属性的值
*/
public String getSingletonData(){
return singletonData;
}
}

(PS:2011-11-16补充)懒汉式单例实现在JAVA API中的应用

View Code
package java.awt;
public class Desktop {
public static synchronized Desktop getDesktop(){
if (GraphicsEnvironment.isHeadless()) throw new HeadlessException();
if (!Desktop.isDesktopSupported()) {
throw new UnsupportedOperationException("Desktop API is not " +
"supported on the current platform");
}

sun.awt.AppContext context = sun.awt.AppContext.getAppContext();
Desktop desktop = (Desktop)context.get(Desktop.class);

if (desktop == null) {
desktop = new Desktop();
context.put(Desktop.class, desktop);
}

return desktop;
}
}

2.2饿汉式单例实现的示例(第二种单例模式)

View Code
/**
* 饿汉式单例实现的示例
*/
public class Singleton {
/**
* 定义一个变量来存储创建好的类实例,直接在这里创建类实例,只会创建一次
*/
private static Singleton uniqueInstance = new Singleton();
/**
* 私有化构造方法,好在内部控制创建实例的数目
*/
private Singleton(){
//
}
/**
* 定义一个方法来为客户端提供类实例
*
@return 一个Singleton的实例
*/
public static Singleton getInstance(){
//直接使用已经创建好的实例
return uniqueInstance;
}

/**
* 示意方法,单例可以有自己的操作
*/
public void singletonOperation(){
//功能处理
}
/**
* 示意属性,单例可以有自己的属性
*/
private String singletonData;
/**
* 示意方法,让外部通过这些方法来访问属性的值
*
@return 属性的值
*/
public String getSingletonData(){
return singletonData;
}
}

(PS:2011-11-16补充)饿汉式单例实现在JAVA API中的应用

View Code
package java.lang;

public class Runtime {
private static Runtime currentRuntime = new Runtime();
public static Runtime getRuntime() {
return currentRuntime;
}
private Runtime() {}
}

2.3关于饿汉式和懒汉式的名称说明:

所谓饿汉式,既然饿,那么在创建单例对象实例(uniqueInstance的时候就比较着急,饿了嘛,于是就在装载类的时候就创建单例对象实例(uniqueInstance),写法如下:

private static Singleton uniqueInstance = new Singleton(); 

所谓懒汉式,既然懒,那么创建单例的对象实例的时候就不着急,会一直等到即将要使用单例对象实例的时候才会创建,懒人嘛,总会推脱不开的时候才采取真正执行工作,因此在装载对象的时候不创建对象实例,写法如下:

private static Singleton uniqueInstance = null; 

懒汉式是在调用对象方法getInstance()的时候采取创建单例对象实例uniqueInstance,写法如下所示:

View Code
public static synchronized Singleton getInstance(){
//判断存储实例的变量是否有值
if(uniqueInstance == null){
//如果没有,就创建一个类实例,并把值赋值给存储类实例的变量
uniqueInstance = new Singleton();
}
//如果有值,那就直接使用
return uniqueInstance;
}

2.4应用实例

通过一个读取配置文件的单例来说明单例模式的具体使用方法,这里选择使用饿汉式单例实现方式。

单例AppConfig

View Code
import java.io.*;
import java.util.*;
/**
* 读取应用配置文件,单例实现
*/
public class AppConfig {
/**
* 定义一个变量来存储创建好的类实例,直接在这里创建类实例,只会创建一次
*/
private static AppConfig instance = new AppConfig();
/**
* 定义一个方法来为客户端提供AppConfig类的实例
*
@return 一个AppConfig的实例
*/
public static AppConfig getInstance(){
return instance;
}

/**
* 用来存放配置文件中参数A的值
*/
private String parameterA;
/**
* 用来存放配置文件中参数B的值
*/
private String parameterB;

public String getParameterA() {
return parameterA;
}
public String getParameterB() {
return parameterB;
}
/**
* 私有化构造方法
*/
private AppConfig(){
//调用读取配置文件的方法
readConfig();
}
/**
* 读取配置文件,把配置文件中的内容读出来设置到属性上
*/
private void readConfig(){
Properties p = new Properties();
InputStream in = AppConfig.class.getResourceAsStream("AppConfig.properties");
try {
p.load(in);
//把配置文件中的内容读出来设置到属性上
this.parameterA = p.getProperty("paramA");
this.parameterB = p.getProperty("paramB");
} catch (IOException e) {
System.out.println("装载配置文件出错了,具体堆栈信息如下:");
e.printStackTrace();
}
}

}

客户端Client

View Code
public class Client {
public static void main(String[] args) {
//创建读取应用配置的对象
AppConfig config = AppConfig.getInstance();

String paramA = config.getParameterA();
String paramB = config.getParameterB();

System.out.println("paramA="+paramA+",paramB="+paramB);
}
}

配置文件AppConfig.properties

paramA=a1
paramB=b2

3.懒汉式和饿汉式的具体实现

在前面的代码实例中我们可以看到, 获取对象实例的这个方法getInstance()是一个实例方法,也就是说客户端要想调用这个方法,需要先得到实例,然后才可以通过这个实例来调用getInstance()方法。可是这个方法本身就是为了得到类实例的,这样就形成了一个死循环,成了典型的“先有鸡还是先有蛋”的问题。要解决这个问题也很简单,只需要在getInstance()方法前面加上static,这样就可以直接通过类来调用这个方法,而不需要先得到实例。

3.1懒汉式实现

因为成员变量uniqueInstance在静态方法中被使用,那么这个成员变量被迫称为一个类变量,要强制加上static。也就是说,在懒汉式实现中的uniqueInstance并没有使用static的特性。懒汉式的完整实现如下所示,为了便于理解,在注释中标示了代码的先后顺序:

View Code
public class Singleton {
    //4:定义一个变量来存储创建好的类实例
    //5:因为这个变量要在静态方法中使用,所以需要加上static修饰
    private static Singleton instance = null;
    //1:私有化构造方法,好在内部控制创建实例的数目
    private Singleton(){
    }
    //2:定义一个方法来为客户端提供类实例
    //3:这个方法需要定义成类方法,也就是要加static
    public static  Singleton getInstance(){
        //6:判断存储实例的变量是否有值
        if(instance == null){
            //6.1:如果没有,就创建一个类实例,并把值赋值给存储类实例的变量
            instance = new Singleton();
        }
        //6.2:如果有值,那就直接使用
        return instance;
    }
}

3.2饿汉式实现

Java中static的特性:

  • static变量在类装载的时候进行初始化
  • 多个实例的static变量会共享同一块内存区域

这就意味着,在java中,static变量只会被初始化一次,就是在类装载的时候,而且多个实例都会共享这个内存,这不就是单例模式要实现的吗?利用static的特性,我们就能够控制值创造一个实例。代码的具体写法如下所示,同样标注了代码的先后顺序:

View Code
public class Singleton {
//4:定义一个静态变量来存储创建好的类实例
//直接在这里创建类实例,只会创建一次
private static Singleton instance = new Singleton();
//1:私有化构造方法,好在内部控制创建实例的数目
private Singleton(){
}
//2:定义一个方法来为客户端提供类实例
//3:这个方法需要定义成类方法,也就是要加static
public static Singleton getInstance(){
//5:直接使用已经创建好的实例
return instance;
}
}

4单例模式的优缺点

4.1时间和空间

  • 懒汉式是典型的时间换空间,也就是每次获取实例都会进行判断,看是否需要创建实例,浪费判断的时间。当然,如果一直不使用实例,那就不会创建实例,从而节约了内存空间。
  • 饿汉式是典型的空间换时间,当类装载的时候就会创建类实例,不管你用不用,先创建出来,然后每次调用的时候,不用进行判断,直接返回类实例即可,节约了运行时间。

4.2线程安全

从线程安全上来讲,不加同步的懒汉式是线程不安全的,3.1中的懒汉式实现就是线程不安全的。饿汉式是线程安全的,因为虚拟机保证只装载一次,在装载类的时候是不会发生并发的。那么如何实现懒汉式的线程安全呢?只需要加上synchronized 关键字即可,写法如下(可以参考2.1中的写法):

public static synchronized Singleton getInstance(){}  

但是这种写法有一个弊端,那就是每次调用这个getInstance()方法的时候都需要同步,影响性能。更好的方法是使用双重检查加锁机制。这种方法即能保证线程安全,又能够使性能不受太大影响。

所谓双重检查加锁,值得就是:并不是每次进入getInstance()方法都需要同步,而是先不同步,进入方法后,先检查实例是否存在,如果不存在才进入下面的同步块,这是第一重检查。进入同步块后,再次检查视力是否存在,如果不存在,就在同步的情况下创建一个实例,这就是第二重检查。这样一来,只需要同步一次即可(因为同步完必定能够创建一个实例,后面instance就不为null了。),从而减少了多次在同步情况下进行判断所浪费的时间。

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

4.3双重检查加锁单例模式示例(第三种单例模式)

 

View Code
public class Singleton {
/**
* 对保存实例的变量添加volatile的修饰
*/
private volatile static Singleton instance = null;
private Singleton(){

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

5.更好的单例实现模式

前面介绍的几种单例实现方式都存在小小的缺陷,那么有没有一种方案,既能够实现延迟加载,又能够实现线程安全?确实存在这样一种解决方案,我们将它称之为Lazy initialization holder class模式,这个模式综合使用了java的类级内部类和多线程缺省同步锁的知识,很巧妙地同时实现了延迟加载和线程安全。

5.1类级内部类单例模式示例(第四种单例模式)

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

public static Singleton getInstance(){
return SingletonHolder.instance;
}
}

5.2类级内部类的相关知识

  • 什么是类级内部类?简单点说,类级内部类指的是,有static修饰的成员式内部类。如果没有static修饰的成员式内部类称为对象级内部类
  • 类级内部类相当于外部类的static成分,它的对象与外部类对象间不存在依赖关系,因此可直接创建。而对象级内部类的实例,是绑定在外部对象实例中的。(java static解析)
  • 类级内部类中,可定义静态的方法。在静态方法中只能够引用外部类中的静态成员方法或者静态成员变量。
  • 类级内部类相当于外部类的成员,只有在第一次被使用的时候才会被装载。

5.3多线程缺省同步锁的知识

在多线程开发中,为了解决并发问题,主要是通过使用synchronized来加互斥锁进行同步控制。但是在某些情况下,JVM已经隐含地为您执行了同步,这些情况下就不用自己再来进行同步控制了。这些情况包括:

  • 由静态初始化器(在静态字段上或者static{}块中的初始化器)初始化数据时
  • 访问final字段时
  • 在创建线程之前创建对象时
  • 线程可以看见它将要处理的对象时

5.4解决方案的思路

在5.1的第四种单例模式示例中,当getInstance方法第一次被调用的时候,它第一次读取 SingletonHolder.instance,导致类级内部类SingletonHolder得到初始化;而这个类在装载并被初始化的时候,会初始化它的静态域(也就是static修饰的内容),从而创建Singleton的实例,代码如下所示:

private static Singleton instance = new Singleton();  

由于是静态的域,因此只会在虚拟机装载类的时候初始化一次,并有虚拟机来保证它的线程安全。

这种方式利用了classloder的机制来保证初始化instance时只有一个线程,它跟第二种方式不同的是(很细微的差别):第二种方式是只要Singleton类被装载了,那么instance就会被实例化(没有达到lazy loading效果),而这种方式是Singleton类被装载了,instance不一定被初始化。因为SingletonHolder类没有被主动使用,只有显示通过调用getInstance方法时,才会显示装载SingletonHolder类,从而实例化instance。想象一下,如果实例化instance很消耗资源,我想让他延迟加载,另外一方面,我不希望在Singleton类加载时就实例化,因为我不能确保Singleton类还可能在其他的地方被主动使用从而被加载,那么这个时候实例化instance显然是不合适的。这个时候,这种方式相比二种方式就显得很合理。

6.单例与枚举

6.1使用枚举来实现单例模式的示例(第五种单例模式)

View Code
/**
* 使用枚举来实现单例模式的示例
*/
public enum Singleton {
/**
* 定义一个枚举的元素,它就代表了Singleton的一个实例
*/
uniqueInstance;

/**
* 示意方法,单例可以有自己的操作
*/
public void singletonOperation(){
//功能处理
}
}

6.2解析

这种方式是Effective Java作者Josh Bloch 提倡的方式,它不仅能避免多线程同步问题,而且还能防止反序列化重新创建新的对象,可谓是很坚强的壁垒啊,不过,由于1.5中才加入enum特性,用这种方式写不免让人感觉生疏,在实际工作中,我也很少看见有人这么写过。

7.单例枚举实例(2011-12-4)

2.4使用恶汉式单例模式写了一个读取配置文件的案例,接下来我们使用枚举单例模式改写2.4的实例,完成同样的功能,也就是读取配置文件。只对AppConfig.java进行修改,其他的比如Client和配置文件都不做任何修改。实例如下:

AppConfig.java

View Code
import java.io.*;
import java.util.*;

public enum AppConfig {
instance ;

private String parameterA;
private String parameterB;
public String getParameterA() {
return parameterA;
}
public String getParameterB() {
return parameterB;
}
//构造函数
private AppConfig() {
readConfig();//在构造函数中读取配置文件。
}
//读取配置文件
private void readConfig() {
Properties p = new Properties();
InputStream in = AppConfig.class.getResourceAsStream("AppConfig.properties");
try {
p.load(in);
this.parameterA = p.getProperty("paramA");
this.parameterB = p.getProperty("paramB");
} catch (IOException e) {
System.out.println("装载配置文件出错了,具体堆栈信息如下:");
e.printStackTrace();
}
}
}



posted @ 2011-11-14 08:15  xwdreamer  阅读(1977)  评论(0编辑  收藏  举报