`
java-mans
  • 浏览: 11343927 次
文章分类
社区版块
存档分类
最新评论

设计模式笔记7:单例模式(Singleton Pattern)

 
阅读更多

一、单例模式的内容

单例模式确保一个类只有一个实例,并提供全局访问点。单例模式是一种对象创建型模式。单例模式又名单件模式或单态模式。

二、单例模式特点

  • 某个类只能有一个实例。
  • 它必须自行创建这个实例。
  • 它必须自行向整个系统提供这个实例。

三、单例模式结构图


Singleton模式包含的角色只有一个,就是Singleton。Singleton拥有一个私有构造函数,确保用户无法通过new直接实例它。除此之外,该模式中包含一个静态私有成员变量instance与静态公有方法Instance()。Instance方法负责检验并实例化自己,然后存储在静态成员变量中,以确保只有一个实例被创建。

四、单例模式示例代码

代码一
public class Singleton {
	private static Singleton uniqueInstance;
	// other useful instance variables here
	private Singleton() {}
	public static Singleton getInstance() {
		if (uniqueInstance == null) {
			uniqueInstance = new Singleton();
		}
		return uniqueInstance;
	}
	// other useful methods here
}
代码二
public class Singleton {
	private static Singleton uniqueInstance = new Singleton();
	private Singleton() {}
	public static Singleton getInstance() {
		return uniqueInstance;
	}
}
代码三
public class Singleton {
	private static Singleton uniqueInstance;
	// other useful instance variables here
	private Singleton() {}
	public static synchronized Singleton getInstance() {
		if (uniqueInstance == null) {
			uniqueInstance = new Singleton();
		}
		return uniqueInstance;
	}
	// other useful methods here
}
代码四
//
// Danger!  This implementation of Singleton not
// guaranteed to work prior to Java 5
//
public class Singleton {
	private volatile static Singleton uniqueInstance;
	private Singleton() {}
	public static Singleton getInstance() {
		if (uniqueInstance == null) {
			synchronized (Singleton.class) {
				if (uniqueInstance == null) {
					uniqueInstance = new Singleton();
				}
			}
		}
		return uniqueInstance;
	}
}

五、四种代码分析

代码一分析:懒汉式单例类,延迟创建实例,避免了当调用类时就创建实例。如果再如代码二情形在加载时就创建了实例,此时如果没有用到实例,而创建实例时耗费掉了非常多的资源,那么这些资源显然就浪费掉了。这种方式采用getInstance方法的获取实例,避免了潜在的资源浪费,但是当多个线程加载时会出现异常,因为多个线程可能同时访问getInstance()方法时会造成新建多个实例的现象(学过操作系统,知道临界资源的会比较好理解)。
代码二分析:饿汉式单例类,这种方式交上一种方式来讲,有可能会造成资源的浪费,但是在实例的第一次加载速度上会超过前者。同时避免了多线程的问题。
代码三分析:代码三是代码一的BUG修复版,虽然修复了多线程访问时可能产生的BUG但是,效率不高,因为其是串行执行,而非并行执行,并且每次执行都要串行执行,降低了多线程的性能。
代码四分析:代码四是代码三的增强版,使用到了volatile关键字,并且在判断时做了优化,比较符合多线程的情况。

5.1 Volatile关键字

Volatile修饰的成员变量在每次被线程访问时,都强迫从共享内存中重读该成员变量的值。而且,当成员变量发生变化时,强迫线程将变化值回写到共享内存。这样在任何时刻,两个不同的线程总是看到某个成员变量的同一个值。
Java语言规范中指出:为了获得最佳速度,允许线程保存共享成员变量的私有拷贝,而且只当线程进入或者离开同步代码块时才与共享成员变量的原始值对比。
这样当多个线程同时与某个对象交互时,就必须要注意到要让线程及时的得到共享成员变量的变化。
而volatile关键字就是提示VM:对于这个成员变量不能保存它的私有拷贝,而应直接与共享成员变量交互。
使用建议:在两个或者更多的线程访问的成员变量上使用volatile。当要访问的变量已在synchronized代码块中,或者为常量时,不必使用。
由于使用volatile屏蔽掉了VM中必要的代码优化,所以在效率上比较低,因此一定在必要时才使用此关键字。

5.2 饿汉式单例与懒汉式单例类比较

  • 饿汉式单例类在自己被加载时就将自己实例化。单从资源利用效率角度来讲,这个比懒汉式单例类稍差些。从速度和反应时间角度来讲,则比懒汉式单例类稍好些。
  • 懒汉式单例类在实例化时,必须处理好在多个线程同时首次引用此类时的访问限制问题,特别是当单例类作为资源控制器,在实例化时必然涉及资源初始化,而资源初始化很有可能耗费大量时间,这意味着出现多线程同时首次引用此类的机率变得较大,需要通过同步化机制进行控制。

5.3 补充

public class Singleton {
	
	private static final Singleton INSTANCE = new Singleton();
	
	private Singleton() {
		if(INSTANCE != null) {
			throw new IllegalStateException("Trying to get more than one instance.");
		}
	}
	
	public static Singleton getInstance() {
		return INSTANCE;
	}
}
这种方式可以防止用反射调用私有构造方法来创建多个对象,却不能防止通过反序列化得到多个对象,《Effective Java》提倡使用enum实现单例:
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
public class Test {
	public static void main(String[] args) throws Exception {
		Singleton d1 = Singleton.INSTANCE;
		d1.setName("a fucker.");
		System.out.println(d1);
		
		FileOutputStream fos = new FileOutputStream("out.data");
		ObjectOutputStream oos = new ObjectOutputStream(fos);
		oos.writeObject(d1);
		fos.close();
		oos.close();
		
		FileInputStream fis = new FileInputStream("out.data");
		ObjectInputStream ois = new ObjectInputStream(fis);
		Object o = ois.readObject();
		fis.close();
		ois.close();
		
		Singleton d2 = (Singleton)o;
		
		System.out.println(d2);
		System.out.println(d1 == d2);
	}
}
enum Singleton implements Serializable {
	
	INSTANCE;
	
	private String name;
	public String getName() {
		return name;
	}
	public void setName(String name) {
		this.name = name;
	}
	@Override
	public String toString() {
		return "[" + name + "]";
	}
}
因为一个enum常量(这里是INSTANCE)代表了一个enum的实例,enum类型只能有这些常量实例。标准保证enum常量(INSTANCE)不能被克隆,也不会因为反序列化产生不同的实例,想通过反射机制得到一个enum类型的实例也不行的。

六、单例模式优缺点

优点
  • 提供了对唯一实例的受控访问。因为单例类封装了它的唯一实例,所以它可以严格控制客户怎样以及何时访问它,并为设计及开发团队提供了共享的概念。
  • 由于在系统内存中只存在一个对象,因此可以节约系统资源,对于一些需要频繁创建和销毁的对象,单例模式无疑可以提高系统的性能。
  • 允许可变数目的实例。我们可以基于单例模式进行扩展,使用与单例控制相似的方法来获得指定个数的对象实例。
缺点
  • 由于单例模式中没有抽象层,因此单例类的扩展有很大的困难。
  • 单例类的职责过重,在一定程度上违背了“单一职责原则”。因为单例类既充当了工厂角色,提供了工厂方法,同时又充当了产品角色,包含一些业务方法,将产品的创建和产品的本身的功能融合到一起。
  • 滥用单例将带来一些负面问题,如为了节省资源将数据库连接池对象设计为单例类,可能会导致共享连接池对象的程序过多而出现连接池溢出;现在很多面向对象语言(如Java、C#)的运行环境都提供了自动垃圾回收的技术,因此,如果实例化的对象长时间不被利用,系统会认为它是垃圾,会自动销毁并回收资源,下次利用时又将重新实例化,这将导致对象状态的丢失。

七、单例模式适用环境

在以下情况下可以使用单例模式:
  1. 系统只需要一个实例对象,如系统要求提供一个唯一的序列号生成器,或者需要考虑资源消耗太大而只允许创建一个对象。
  2. 客户调用类的单个实例只允许使用一个公共访问点,除了该公共访问点,不能通过其他途径访问该实例。
  3. 在一个系统中要求一个类只有一个实例时才应当使用单例模式。反过来,如果一个类可以有几个实例共存,就需要对单例模式进行改进,使之成为多例模式。

八、使用单例时需要注意的

  • 不要使用单例模式存取全局变量。这违背了单例模式的用意,最好放到对应类的静态成员中。
  • 不要将数据库连接做成单例,因为一个系统可能会与数据库有多个连接,并且在有连接池的情况下,应当尽可能及时释放连接。Singleton模式由于使用静态成员存储类实例,所以可能会造成资源无法及时释放,带来问题。


九、单例模式应用

(1) java.lang.Runtime类
public class Runtime {
    private static Runtime currentRuntime = new Runtime();
    public static Runtime getRuntime() { 
	return currentRuntime;
    }
    private Runtime() {}
    ......
} 
(2) 一个具有自动编号主键的表可以有多个用户同时使用,但数据库中只能有一个地方分配下一个主键编号,否则会出现主键重复,因此该主键编号生成器必须具备唯一性,可以通过单例模式来实现。
(3) 默认情况下,Spring会通过单例模式创建bean实例:
<bean id="date" class="java.util.Date" scope="singleton"/>

Singleton(recognizeable by creational methods returning thesameinstance (usually of itself) everytime)

十、参考文献


分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics