单例模式简介
单例模式是软件设计模式中最简单的一种设计模式。从名称中可以看出,单例的目的就是使系统中只能包含有一个该类的唯一一个实例。
单例模式最初的定义出现于《设计模式》(艾迪生维斯理, 1994):“保证一个类仅有一个实例,并提供一个访问它的全局访问点。”
Java中单例模式定义:“一个类有且仅有一个实例,并且自行实例化向整个系统提供。”
对于系统中的某些类来说,只有一个实例很重要,例如,一个系统中可以存在多个打印任务,但是只能有一个正在工作的任务;一个系统只能有一个窗口管理器或文件系统;一个系统只能有一个计时工具或ID(序号)生成器。如在Windows中就只能打开一个任务管理器。如果不使用机制对窗口对象进行唯一化,将弹出多个窗口,如果这些窗口显示的内容完全一致,则是重复对象,浪费内存资源;如果这些窗口显示的内容不一致,则意味着在某一瞬间系统有多个状态,与实际不符,也会给用户带来误解,不知道哪一个才是真实的状态。因此有时确保系统中某个对象的唯一性即一个类只能有一个实例非常重要。
本文将以Java语言来描述单例类的几种实现方式,以及分析给出的几种实现方式的优缺点和并用程序验证性能数据。
Java实例化的方式
我们知道在Java语言中创建实例的方法大体上有4种:
1、使用new关键字
import java.io.Serializable;
import java.util.Random;
/**
* @author thinkpad
*
*/
public class Singleton implements Serializable{
private static final long serialVersionUID = -3868390997950184335L;
private static Random random = new Random();
private int id;
private String name;
private int [] data = {
random.nextInt(),
random.nextInt(),
random.nextInt()
};
//default constructor
public Singleton(){
}
//public constructor
public Singleton(int id){
this.id = id * 2;
}
//private constructor
private Singleton(int id,String name){
this(id);
this.name = name + "." + name;
}
@Override
public String toString(){
StringBuffer buf = new StringBuffer();
buf.append(super.toString()).append("\n")
.append("id :").append(id).append("\n")
.append("name :").append(this.name).append("\n")
.append("data array :").append(data.toString());
return buf.toString();
}
}
最常见的使用new 关键字,创建一个对象
public class TestSingleton {
public static void main(String[] args) {
Singleton single = new Singleton(1);
}
}
2、使用反射
使用反射能够突破JAVA中可见权限,本示例中调用了Singleton私有的构造方法。
import java.lang.reflect.Constructor;
import java.lang.reflect.Type;
/**
* @author thinkpad
*
*/
public class TestSingletonUseReflect {
public static void main(String[] args) {
try {
Constructor<?>[] constructors = Singleton.class.getDeclaredConstructors();
System.out.println(constructors.length);
for(Constructor<?> con : constructors ) {
Type[] types = con.getParameterTypes();
if (types.length == 2){
con.setAccessible(true);
Singleton singleton = null;
singleton = (Singleton) con.newInstance(1,"single1");
System.out.println(singleton);
Singleton singleton2 = null;
singleton2 = (Singleton) con.newInstance(2,"single2");
System.out.println(singleton2);
}
}
}catch(Exception e){
e.printStackTrace();
}
}
}
3、使用反序列化
我们知道Java对象序列化(Object Serialization)将那些实现了Serializable接口的对象转换成一个字节序列,并可以通过反序列化将这个字节序列完全恢复成为原来的对象,因此反序列化提供了一个创建对象的方式。由于使用反序列化创建对象和使用new关键字创建对象有一些不同,反序列化过程中构造方法是没有被调用的(也不一定,若序列化对象父类没有实现Serializable接口,例如Object类,序列化过程中会递归调用父类的无参构造函数),而且其中的域的初始化代码也没有被执行。
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
/**
* @author thinkpad
*
*/
public class TestSerializable {
/**
* @param args
*/
public static void main(String[] args) {
Singleton single1 = new Singleton(100);
Singleton single2 = new Singleton(200);
try {
ByteArrayOutputStream buf = new ByteArrayOutputStream();
ObjectOutputStream out = new ObjectOutputStream(buf);
System.out.println("**********begin serializable**********");
System.out.println(single1);
System.out.println(single2);
out.writeObject(single1);
out.writeObject(single2);
out.close();
System.out.println("**********begin unserializable**********");
ObjectInputStream in = new ObjectInputStream(new ByteArrayInputStream(buf.toByteArray()));
Singleton s1 = (Singleton)in.readObject();
Singleton s2 = (Singleton)in.readObject();
System.out.println(s1.toString());
System.out.println(s2.toString());
in.close();
} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}
4、依赖注入(Spring DI)
依赖注入从JVM的角度来看的话,应该不算是一种创建对象的方式。关于依赖注入的概念,请参考Spring官方网站。
Java单例的几种实现方式
根据单例模式的定义及描述,要实现单例模式,有几个问题是需要考虑的。单例如何创建以及由谁创建,如何保证实例的唯一性,如何提供全局的访问点。由于Java语言中的实例化有多种,所以Java单例必须能够保证在各种条件下维持instance的唯一性,能够做到线程安全、延迟加载并且能够很好的抵抗反射攻击、反序列化攻击等。本文的以下内容将分析和验证常见的几种单例的设计方法,并用代码来验证效果(网上理论讲的很多,但是大部分都缺少实际的代码验证)。
实现单例,必须是私有的构造器,并导出公有的静态实例,根据静态实例的初始化方式,可以分为饿汉型和懒汉型,根据实例的暴露方式可以分为直接暴露和工厂方法间接暴露。饿汉型在类加载的时候就完成实例化,懒汉型就是在实际使用的时候才进行实例化。因此饿汉型的单例是能够保证线程安全的(JVM来保证ClassLoader只加载一次,不考虑多个ClassLoader的情况),懒汉型的线程安全却需要额外的同步来控制;饿汉型不能实现懒汉式的延迟加载。
1、直接暴露-饿汉型(1)
这种写法非常简单、粗暴,而且也能较好的满足需要,呵呵,有时候简单粗暴也是最能快速解决问题的。这种方式线程安全,但是缺乏灵活性,不容易扩展。
import java.io.Serializable;
public class DirectHungrySingleton implements Serializable{
private static final long serialVersionUID = 6069877357741265707L;
//实例对象
public static final DirectHungrySingleton singleton = new DirectHungrySingleton();
//私有的构造方法
private DirectHungrySingleton(){
}
}
2、工厂方法-饿汉型
import java.io.Serializable;
public class FactoryMethodHungrySingleton implements Serializable{
private static final long serialVersionUID = 6069877357741265707L;
//私有的实例对象
private static final FactoryMethodHungrySingleton singleton = new FactoryMethodHungrySingleton();
//私有的构造方法
private FactoryMethodHungrySingleton(){
}
//公有工厂方法
public static FactoryMethodHungrySingleton getInstance(){
return singleton;
}
}
import java.io.Serializable;
public class FactoryMethodLazySingleton implements Serializable{
private static final long serialVersionUID = 6069877357741265707L;
//私有的实例对象
private static FactoryMethodLazySingleton singleton = null;
//私有的构造方法
private FactoryMethodLazySingleton(){
}
//4.增加线程安全性 【 在类对象上加锁,控制并发访问,防止由于实例化过程中(new对象和初始化过程中还未返回),其他线程调用getSingleton方法,导致存在多个实例】
public static synchronized FactoryMethodLazySingleton getInstance(){
if (singleton == null){
singleton = new FactoryMethodLazySingleton();
}
return singleton;
}
}
4、工厂方法-懒汉型(二) 使用双重检查(适用jdk1.5+)
我们分析代码发现,只需要在实例化过程中控制不能让多个线程进入即可,根据锁定的粒度最小化原则,只需要在new 代码行外加锁即可。这是著名的双重检查写法,一些网站资料上显示只适用于jdk1.5+,我个人的理解是jdk1.5+后,volatile关键字功能的增强,能够保证多线程环境下变量读写的可见性。但是具体资料还未找到,请知道的朋友告知一下。
import java.io.Serializable;
public class FactoryMethodLazyDCSingleton implements Serializable{
private static final long serialVersionUID = 6069877357741265707L;
//私有的实例对象
//使用volatile保证多线程情况下的可见性
private static volatile FactoryMethodLazyDCSingleton singleton = null;
//私有的构造方法
private FactoryMethodLazyDCSingleton(){
}
//公有工厂方法
public static FactoryMethodLazyDCSingleton getInstance(){
//外层检查,避免实例化成功后进入同步块,提高性能
if (singleton == null){
synchronized(FactoryMethodLazyDCSingleton.class){
//内层检查,避免多个线程进入后,重复实例化
if (singleton == null)
singleton = new FactoryMethodLazyDCSingleton();
}
}
return singleton;
}
}
5、工厂方法-懒汉型(三) 使用内部静态类
使用双重检查能够很好的满足线程安全和延迟加载,但是代码书写比较复杂、容易出错,在网上出现了一种使用内部静态类的写法,感觉非常优雅。它通过内部类的延迟加载特性实现lazy loading,同时借助JVM的类加载机制保证线程安全,是一种非常优雅的写法。
import java.io.Serializable;
public class FactoryMethodLazyInnerClassSingleton implements Serializable {
private static final long serialVersionUID = 6069877357741265707L;
//内部静态类
private static class Holder {
static final FactoryMethodLazyInnerClassSingleton singleton
= new FactoryMethodLazyInnerClassSingleton();
}
// 私有的构造方法
private FactoryMethodLazyInnerClassSingleton() {
}
// 公有工厂方法
public static FactoryMethodLazyInnerClassSingleton getInstance() {
return Holder.singleton;
}
}
6、使用Enum (Jdk 1.5+)
在jdk1.5中提供了Enum关键字来定义枚举类型,我们知道枚举类是一种特殊的类。它有以下这些特点,为了保证类型安全,不对外提供公有的构造方法,所有的枚举对象均在类加载时完成初始化,并且均为static final类型,这些特点完全类似于第1种写法,简单优雅,线程安全,并完全提供抵抗反序列化攻击的机制。
//jdk1.6 Enum类 源码第199行可见 无法对enum进行反序列化,保证了唯一性
/**
* prevent default deserialization
*/
private void readObject(ObjectInputStream in) throws IOException,
ClassNotFoundException {
throw new InvalidObjectException("can't deserialize enum");
}
public enum EnumSingleton {
//定义实例
INSTANCE(1, "other");
//定义其他域
private int intField;
private String otherField;
private EnumSingleton(int intField, String otherField) {
this.intField = intField;
this.otherField = otherField;
System.out.println("init the enum type");
}
@Override
public String toString() {
return new StringBuffer().append(super.toString()).append(intField)
.append(otherField).toString();
}
}