设计模式(三)----单例模式

简介

单例模式(Singleton Pattern),可以说是Java当中最耳熟能详的一种设计模式,也是最简单的一种设计模式。这种模式涉及到一个单一的类,该类负责创建自己的对象,同时要确保只有一个对象被创建。这个类提供了访问该类对象的唯一的一种方式,就是通过该类对外的接口来访问。
注:单例类只能有一个实例,并且只能自己创建该实例,同时也必须给其他所有对象提供该实例。
意图:保证一个类只有一个实例,并且提供一个全局访问点。
主要解决:一个全局的类被频繁的创建和销毁。
使用场景:控制实例数目,节省系统资源时,比如I/O和数据库的连接。
关键代码:构造函数私有化(避免在单例类之外的其他类中创建该类对象)。
优点:内存中只有一个实例,减少了内存开销,尤其是频繁的创建和销毁,避免了对象资源的多重占用。
缺点:没有接口,不能继承,与单一职责原则相冲突,一个类只应该关心内部实现,而不应该关心外面如何实例化。
下面列出单例模式主要的几种实现方式

饿汉式

这种方式实现的单例是线程安全的,但没有实现Lazy Loading(懒加载)
比较常用的一种方式,但容易产生垃圾对象。
优点:没有加锁,执行效率高。
缺点:类加载时就初始化,浪费内存。
此种方式基于classloader机制避免了多线程的同步问题,不过,instance在类装载时就实例化,虽导致类装载的原因有很多种,单例模式中多数都是调用getInstance()方法,但也不能确定会有其他方式导致类加载,这时候初始化实例对象显然没有达到Lazy Loading效果。
实例代码

public class HungrySingleton {
	private HungrySingleton() {}
	private static HungrySingleton instance = new HungrySingleton();
	public static HungrySingleton getInstance() {
		return instance;
	}
}

懒汉式

这种方式实现了Lazy Loading,有线程安全和线程不安全两种写法,线程不安全的写法因为没有加锁synchronized,所以严格意义上并不算单例模式(多线程下不能保证只有一个对象被创建)。
线程安全实例代码

public class LazySingleton {
	private LazySingleton() {}
	private static LazySingleton instance;
	public static synchronized LazySingleton getInstance() {
		if(instance == null) {
			instance = new LazySingleton();
		}
		return instance;
	}
}

这种方式在多线程下也能够很好的工作,但效率很低,99%的情况下不需要同步,加锁对效率影响较大。
第一次调用才初始化,有效避免了内存浪费,用于getInstance方法调用不太频繁的情况下。
线程不安全写法就是去掉getInstance方法中的synchronized关键字,其他与上面写法相同。

双检锁/双重校验锁(double-checked locking DCL)

这种方式是线程安全的,并且实现了Lazy Loading
采用双锁机制,能在多线程下保持高性能。
实例代码

public class DCLSingleton {
	private DCLSingleton() {}
	private volatile static DCLSingleton instance;
	public static DCLSingleton getInstance() {
		if (instance == null) {
			synchronized(DCLSingleton.class) {
				if (instance == null) {
					instance = new DCLSingleton();
				}
			}
		}
		return instance;
	}
}

登记式/静态内部类

这种方式是线程安全的,并且实现了Lazy Loading
这种方式能达到与双重校验锁同样的效果,且实现更加简单。对静态域使用延迟初始化,应该使用这种方式而非双重校验锁的方式。这种方式只适用于静态域的情况,双重校验锁的方式可在实例域需要延迟初始化的时候使用。
这种方式同样利用了classloader机制保证初始化instance时只有一个线程,它跟饿汉式不同的是,饿汉式只要单例类被装载了,就会实例化instance(没有实现Lazy Loading),而这种方式因为SingletonHolder类没有被主动使用,只有getInstance方法被显式调用,才会显式装载SingletonHolder,从而实例化instance。若实例化instance比较消耗资源,使用该种方式比饿汉式要合理。
实例代码

public class StaticSingleton {
	private StaticSingleton() {}
	private static class SingletonHolder {
		private static final StaticSingleton instance = new StaticSingleton();
	}
	public static final StaticSingleton getInstance() {
		return SingletonHolder.instance;
	}
}

枚举

这种方式是线程安全的,但没有实现Lazy Loading
这种方式还未被广泛采用,但是实现单例的最好方式。它更简洁,自动支持序列化机制,防止反序列化重新创建新的对象(防止多次实例化)。
这种方式是《Effective Java》作者提倡的方式,但实际工作当中很少使用。
不能通过reflection attack来调用私有构造方法。

public enum EnumSingleton {
	INSTANCE;
	public void getInstance() {}
}

总结:

不建议使用懒汉式,建议使用饿汉式。只有在明确需要实现Lazy Loading时,才使用登记方式。若涉及到反序列化创建对象,可尝试使用枚举方式。若有其他特殊要求,可使用双重校验锁方式。

通过序列化或反射可能会生成单例类的多个实例(破坏单例),下面介绍使用序列化方式生成单例类的多个实例
单例类

/**
*实现Serializable接口的方式实现序列化与反序列化
*/
public class SingletonObject implements Serializable {
	//显式声明版本序列号
	private static final long seriaVersionUID = 1L;
	private SingletonObject() {}
	private static SingletonObject instance = new SingletonObject();
	public static SingletonObject getInstance() {
		return instance;
	}
}

测试类

public class SingletonSerializableDemo {
	public static void main(String[] args) {
		SingletonObject obj1 = SingletonObject.getInstance();
		SingletonObject obj2 = null;
		try {
			//序列化
			File file = new File("D:\\JavaTest\\singletonSerializable.txt");
			ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(file));
			oos.writeObject(obj1);
			oos.flush();
			oos.close();
			
			//反序列化
			ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file));
			obj2 = (SingletonObject)ois.readObject();
			ois.close();
		} catch (Exception e) {
			e.printStackTrace();
		}
		System.out.println(obj1 == obj2);
	}
}

打印结果
false
说明反序列化得到了单例类的不同的实例,这不就破坏了单例嘛。
解决办法
最好的解决方法就是使用上面介绍的用枚举法实现单例,这种实现方式支持序列化,能够有效解决因序列化或反射而造成的破坏单例的问题。
若只想解决因序列化而导致的单例破坏问题,可在单例类中加readResolve()方法,单例类代码改造如下

public class SingletonObject implements Serializable {
	private static final seriaVersionUID = 1L;
	private SingletonObject() {}
	private static SingletonObject instance = new SingletonObject();
	//关键代码
	protected Object readResolve() {
		return instance;
	}
	public static SingletonObject getInstance() {
		return instance;
	}
}

此时,再用相同的测试类测试,打印结果为true
readResolve()方法说明:
该方法在反序列化方法readObject()之后执行,所以可以在反序列化之后对所得的对象进行一些处理,甚至直接替换掉原来的对象。在jdk的ObjectInputStream中有readUnshared()方法,若被反序列化的对象的类存在readResolve()方法,它就会调用这个方法来返回一个"array"(此处俺也不懂),然后浅拷贝一份,作为返回值,并且无视反序列化的值,即使那个字节码已经被解析。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值