java中的单例模式

最近开始看《Effective Java》,看到"用私有构造器或者枚举类型强化Singleton属性"一节,其中提到用枚举实现单例模式,然后回头学习了下java中实现单例模式的几种方式,现做如下总结做个笔记

方式一:

简单的早期加载方式(饿汉式)

public class Singleton {
	private Singleton () {}
	
	public static final Singleton INSTANCE = new Singleton(); //公有静态成员	
}
//调用方式:Singleton.INSTANCE

public class Singleton {
	private Singleton () {}
	
	private static final Singleton instance = new Singleton();
	
	public static Singleton getInstance () { //公有静态工厂
		return instance;
	}
}
//调用方式:Singleton.getInstance()

这两种写法是《Effective Java》书中的,书中作者讲到公有静态工厂方法相比于公有静态成员方法提供了一定的灵活性:在不改变其API的前提下,我们可以改变该类是否应该为Singleton的想法,工厂方法返回该类的唯一实例,但是,它很容易被修改,比如改成为每个调用该方法的线程返回一个唯一实例

这种方式看上去虽然简单,但存在些问题,一方面无论这个类是否被使用,在类加载的时候(比如大多数在程序启动时)都要去创建一个实例出来,如果这个实例的创建很耗时或是耗资源,而且这个实例还不一定被使用,或是创建完又迟迟不使用,那这个创建可能就拖了程序的性能(比如导致程序启动很慢)和导致资源浪费;另一方面,有些类的实例的创建可能依赖于程序运行起来后的一些数据或条件,那就无法在程序启动时类加载期间创建出正确的实例出来

但这种方式也有它的好处,就像看上去的那样简单,而且是线程安全的,因为单例实例的创建是在类加载时创建,而JVM内部为我们保证了类的加载过程是线程互斥的,即线程安全。所以一些情况下也可以权衡考虑使用此方式

方式二:

推迟加载方式(lazy loading)(懒汉式)

为避免早期加载方式易产生的问题,通常采用推迟加载方式来实现单例模式,也就是在有使用的时候才去创建这个实例。

//1、非线程安全的推迟加载方式(lazy loading)
public class Singleton {
	private Singleton () {}
	
	private static Singleton instance = null;
	
	public static Singleton getInstance () {
		if (instance == null) {
			instance = new Singleton();
		}
		return instance;
	}
}

//2、线程安全但低效的推迟加载方式
public class Singleton {
	private Singleton () {}
	
    private static Singleton instance = null;
	
	public static synchronized Singleton getInstance () {
		if (instance == null) {
			instance = new Singleton();
		}
		return instance;
	}
}

//3、非线程安全的改进低效同步的方式
public class Singleton {
	private Singleton () {}
	
	private static Singleton instance = null;
	
	public static Singleton getInstance () {
		if (instance == null) {
			synchronized (Singleton.class) {
				instance = new Singleton();
			}
		}
		return instance;
	}
}

//4、线程安全改进低效同步的推迟加载方式(双重校验锁double-check-lock)
public class Singleton {
	private Singleton () {}
	
	private volatile static Singleton instance = null; //要加volatile修饰符
	
	public static Singleton getInstance () {
		if (instance == null) { //first check
			synchronized (Singleton.class) {
				if (instance == null) { //second checks
					instance = new Singleton();
				}
			}
		}
		return instance;
	}
}

可以很容易看出,1的实现方式不是线程安全的,2在1的基础上对getInstance整个方法用synchronized关键字进行了同步,然而在多个线程同时调用getInstance方法时,不管这个单例是否已创建只有一个线程可进入,而实际上只要单例实例被创建后只进行读取操作就没必要进行同步了,只需保证instance = new Singleton();是线程互斥的即可,同步getInstance整个方法无疑大大降低了程序的性能

3在2的基础上再次改进,只有在单例实例尚未创建时才同步,这样看起来是不会降低多大性能的了,但是当有多个线程通过if (instance == null)的判断后,当其中一个进入同步块执行完intance = new Singleton()后,通过中的另一个也会接着进入这个同步块执行同样操作instance = new Singleton(),这样还是会创建出多个实例,非线程安全的
4在3的基础上再次改进,将if (instance == null) 这个条件也同步进来,这样1、2、3的问题就都避免啦,这种就是被称为“Double-Check”的方式了

然而这种方式仍然可能发生错误,这源于JVM的一些机制,对于JVM它执行的是一个个java指令,在java指令中创建对象和赋值操作是分开进行的,也就是说intance = new Singleton()语句是分两步执行的,但JVM并不保证这两个操作的先后顺序,所以也就有可能JVM为Singleton实例先分配空间,然后直接赋给instance成员(此时instance != null,但尚未初始化),然后再去初始化Singleton这个实例,这样就有可能导致某个线程取到尚未被初始化的错误的instance去使用,这如何是好?

好在JDK 5后,Java使用了新的内存模型。volatile关键字有了明确的语义:被volatile修饰的写变量不能和之前的读写代码调整,读变量不能和之后的读写代码调整。因此在4中我们给instance加上volatile关键字就解决了上述问题

方式三:

静态内部类方式

//线程安全的静态内部类实现的推迟加载方式
public class Singleton {
	private Singleton () {}
	
	//静态内部类,该类只有被调用到时才会被加载,从而实现推迟加载,另外JVM保证了类加载是线程安全的
	private static class SingletonHolder {
		private static final Singleton instance = new Singleton();
	}
	
	//只有在第一次调用Singleton.getInstance()时,静态内部类SingletonHolder才会被读取被加载
	//被加载的过程中会初始化其静态域instance,从而创建Singleton实例
	public static Singleton getInstance () {
		return SingletonHolder.instance;
	}
}

此方式也属推迟加载方式,既达到了推迟加载的效果,也保证了多线程的安全,是种很不错的方式

方式四:

枚举方式

//单元素的枚举类型方式的单例
public enum Singleton {
	INSTANCE; //一个枚举的元素,它就代表了Singleton的一个实例,为实现单例模式则只可定义一个枚举元素
	
	public void otherOperator () {}
}
//调用方式:Singleton.INSTANCE

这种方式是《Effective Java》书中作者所提倡的,是目前实现Singleton的最佳方式,但其适用于JDK 5及后续版本中,因为enum是在JDK 5才加入的特性。此方式不仅避免了多线程同步的问题,而且防止了反序列化和反射攻击(下面稍后讲解)以创建多个实例的情况发生

至此Java中单例模式实现的几种方式已大概总结完,下面总结一些上述方式中仍存在的一些问题

1、单例的实现使得构造函数必须声明为私有的,包括枚举其构造函数是只能是私有的,但享有单例使用特权的调用者可以通过反射机制访问私有构造函数来创建实例

//通过反射机制调单例类的私有构造器创建新的实例进行操作
Constructor<Singleton> constructor = Singleton.class.getDeclaredConstructor();
constructor.setAccessible(true);
Singleton anotherInstance = constructor.newInstance();
anotherInstance.operator();
针对这种可能的反射攻击,反射中对通过反射创建枚举实例已做了禁止操作,如下:

// java.lang.reflect.Constructor的newInstance()方法中有如下代码,禁止了通过反射构造枚举对象
if ((clazz.getModifiers() & Modifier.ENUM) != 0)   
    throw new IllegalArgumentException("Cannot reflectively create enum objects");

而采用非枚举的单例实现方式,为避免这种反射攻击,可以修改我们的构造器,在构造器被二次调用创建第二个实例时就直接抛出异常

private volatile static Singleton instance = null; //要加volatile修饰符
	
private Singleton () {
	if (instance != null) {
		throw new IllegalArgumentException("单例只能存在一个实例!");
	}
}

2、如果我们的非枚举方式的Singleton 因需求implements了 Serializable接口,《Effective Java》中有讲到,那么在每次反序列化一个序列化的实例时都会创建一个新的实例出来,这就破坏了单例模式,这种情况下要维护并保证Singleton,则必须声明所有实例域都是transient(瞬时的),并提供实现Serializable接口的readResolve方法

private Object readResolve throws ObjectStreamException () {
	    return INSTANCE;
	}
对此情况找到如下解说:

一般来说, 一个类实现了 Serializable接口, 我们就可以把它往内存地写再从内存里读出而"组装"成一个跟原来一模一样的对象. 不过当序列化遇到单例时,这里边就有了个问题: 从内存读出而组装的对象破坏了单例的规则. 单例是要求一个JVM中只有一个类对象的, 而现在通过反序列化,一个新的对象克隆了出来.如果提供了readResolve方法,这样当JVM从内存中反序列化地"组装"一个新对象时,就会自动调用这个方法来返回我们指定好的对象了, 单例规则也就得到了保证

方法readResolve允许class在反序列化返回对象前替换、解析在流中读出来的对象。实现readResolve方法,一个class可以直接控制反序化返回的类型和对象引用。方法readResolve会在ObjectInputStream已经读取一个对象并在准备返回前调用。ObjectInputStream 会检查对象的class是否定义了readResolve方法。如果定义了,将由readResolve方法指定返回的对象。返回对象的类型一定要是兼容的,否则会抛出ClassCastException 。
对于枚举,可以看下枚举的父类:java.lang.Enum类,其实现了Serializable接口,但却在readObject()和readObjectNoData()方法内直接抛出异常(如果某个类因为继承的原因实现了Serializable接口,而该类却不希望被序列化/反序列化,那么通常可以考虑在readObject()和writeObject()方法中直接抛出异常)

private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {  
    throw new InvalidObjectException("can't deserialize enum");  
}   
private void readObjectNoData() throws ObjectStreamException {  
    throw new InvalidObjectException("can't deserialize enum");  
}
实际上,Java的序列化机制对于枚举类型有特殊的处理,即没有使用普通对象的序列化形式:尽管java.lang.Enum中的name和ordial成员变量都没有声明为transient,实际上序列化过程中写入流的只有name;反序列化过程中通过调用Enum.valueOf(Class<T> enumType, String name)静态方法构造枚举值,从而保证了枚举值的单例性。

所以说枚举方式是实现单例的最佳方式,其有效防止了反射攻击和反序列化破坏单例模式的行为

这里附上关于java中枚举的解说的一个帖子:http://whitesock.iteye.com/blog/728934

单例模式除上述外某些情况下仍然存在其他一些问题,比如多ClassLoader、多JVM的等的,可以看看耗子哥的这篇帖子:深入浅出单实例Singleton设计模式


  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值