创建型:单例模式及相关应用
文章目录
单例模式(Singleton)
保证一个类仅有一个实例,并提供一个全局访问点
适用场景:想确保任何情况下都绝对只有一个实例。
优缺点
优点:在内存里只有一个实例,减少了内存开销;可以避免对资源的多重占用;设置全局访问点,严格控制访问。
缺点:可扩展性较差。
重点
- 私有构造器
- 线程安全
- 延迟加载
- 序列化和反序列化
- 反射
懒汉式实现
线程不安全
以下实现中延迟了lazySingleton的实例化,因此如果没有使用该类,那么就不会实例化lazySingleton,从而节约了资源。
但这种实现是线程不安全的,在多线程的环境下多个线程有可能同时判断if(lazySingleton == null)
为true
而进行实例化,导致多次实例化lazySingleton。
public class LazySingleton {
private static LazySingleton lazySingleton = null;
private LazySingleton(){
}
public static LazySingleton getInstance(){
if(lazySingleton == null){
lazySingleton = new LazySingleton();
}
return lazySingleton;
}
}
synchronized关键字
要想其变为线程安全的,第一种方式是在getInstance()
方法加上synchronized
关键字,使这个方法变为同步方法:
public synchronized static LazySingleton getInstance(){
if(lazySingleton == null){
lazySingleton = new LazySingleton();
}
return lazySingleton;
}
由于这个方法是静态方法,因此这个锁将锁住这个类,等效于以下代码:
public static LazySingleton getInstance(){
synchronized (LazySingleton.class){
if(lazySingleton == null){
lazySingleton = new LazySingleton();
}
}
return lazySingleton;
}
通过这种方式,虽然解决了懒汉式在多线程环境下的同步问题,但由于同步锁消耗的资源较多,且锁的范围较大,对性能有一定影响,因此还需要进行演进。
双重校验锁
当lazyDoubleCheckSingleton就算没有被实例化时,synchronized
关键字也保证了不会出现同步问题,例如,如果两个线程同时判断第一个if(lazyDoubleCheckSingleton == null)
为true
,其中一个线程会进入到第二个if(lazyDoubleCheckSingleton == null)
并开始实例化lazyDoubleCheckSingleton,而另一个线程则被阻塞直到前一个进程释放锁。一旦前一个线程实例化完并释放锁,被阻塞的线程将进入第二个if(lazyDoubleCheckSingleton == null)
且判断为false
。之后,由于lazyDoubleCheckSingleton已经被实例化过,再有线程调用此方法都会在第一个if(lazyDoubleCheckSingleton == null)
就判断为false
,不会再进行加锁操作。
public class LazyDoubleCheckSingleton {
private static LazyDoubleCheckSingleton lazyDoubleCheckSingleton = null;
private LazyDoubleCheckSingleton(){
}
public static LazyDoubleCheckSingleton getInstance(){
if(lazyDoubleCheckSingleton == null){
synchronized (LazyDoubleCheckSingleton.class) {
if (lazyDoubleCheckSingleton == null) {
lazyDoubleCheckSingleton = new LazyDoubleCheckSingleton();
}
}
}
return lazyDoubleCheckSingleton;
}
}
这种实现依然存在问题,对于lazyDoubleCheckSingleton = new LazyDoubleCheckSingleton();
这一行代码其实是分为以下三步执行的:
- 分配内存给这个对象
- 初始化对象
- 设置lazyDoubleCheckSingleton指向刚分配的内存地址
但是JVM为了优化指令,提高程序运行效率,会进行指令重排序,指令顺序有可能由1->2->3变为1->3->2,这在单线程下不会出现问题,但是在多线程下会导致一个线程获得还没有被初始化的实例。例如,一个线程已经执行到了lazyDoubleCheckSingleton = new LazyDoubleCheckSingleton();
这一行,且完成了1->3这两步,即lazyDoubleCheckSingleton已经不为null,但还没有进行初始化,此时另一个线程在第一个if(lazyDoubleCheckSingleton == null)
判断为false
后便将还未被初始化的lazyDoubleCheckSingleton返回,从而产生问题。
要解决指令重排序导致的问题,第一种方式是使用volatile
关键字禁止JVM进行指令重排序:
public class LazyDoubleCheckSingleton {
private volatile static LazyDoubleCheckSingleton lazyDoubleCheckSingleton = null;
private LazyDoubleCheckSingleton(){
}
public static LazyDoubleCheckSingleton getInstance(){
//...
}
}
静态内部类
另一种解决指令重排序所导致的问题的方式是使用静态内部类让其它线程看不到这个线程的指令重排序:
public class StaticInnerClassSingleton {
private static class InnerClass{
private static StaticInnerClassSingleton staticInnerClassSingleton = new StaticInnerClassSingleton();
}
public static StaticInnerClassSingleton getInstance(){
return InnerClass.staticInnerClassSingleton;
}
private StaticInnerClassSingleton(){
}
}
当StaticInnerClassSingleton类加载时,静态内部类InnerClass还不会加载进内存,只有调用getInstance()
方法使用到了InnerClass.staticInnerClassSingleton
时才会加载。在多线程环境下,只有一个线程能获得Class对象的初始化锁,从而加载StaticInnerClassSingleton类,也就是这时候完成staticInnerClassSingleton的实例化,另一个线程此时只能在这个Class对象的初始化锁上等待。因此,由于等待的线程是看不见指令重排序的过程的,所以指令重排的顺序不会有任何影响。
饿汉式实现
饿汉式即当类加载的时候就完成实例化,避免了同步问题,但同时也因为没有延迟实例化的特性而导致资源的浪费。
public class HungrySingleton implements Serializable {
private final static HungrySingleton hungrySingleton = new HungrySingleton();
private HungrySingleton(){
}
public static HungrySingleton getInstance(){
return hungrySingleton;
}
}
以上代码与以下代码等效:
public class HungrySingleton implements Serializable {
private final static HungrySingleton hungrySingleton;
static{
hungrySingleton = new HungrySingleton();
}
private HungrySingleton(){
}
public static HungrySingleton getInstance(){
return hungrySingleton;
}
}
单例模式存在的问题
序列化破坏单例模式
通过对Singleton的序列化与反序列化得到的对象是一个新的对象,这就破坏了Singleton的单例性。
public class Test {
public static void main(String[] args){
HungrySingleton instance = HungrySingleton.getInstance();
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("singleton_file"));
oos.writeObject(instance);
File file = new File("singleton_file");
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file));
HungrySingleton newInstance = (EnumInstance) ois.readObject();
System.out.println(instance);
System.out.println(newInstance);
System.out.println(instance == newInstance);
}
}
之所以会如此,是因为序列化会通过反射调用无参数的构造方法创建一个新的对象。要解决这个问题很简单:只要在Singleton类中定义readResolve即可:
public class HungrySingleton implements Serializable {
//...
private Object readResolve(){
return hungrySingleton;
}
//...
}
反射攻击
通过反射可以打开Singleton的构造器权限,由此实例化一个新的对象。
public class Test {
public static void main(String[] args){
Class objectClass = HungrySingleton.class;
Class objectClass = LazySingleton.class;
Constructor constructor = objectClass.getDeclaredConstructor();
constructor.setAccessible(true);
HungrySingleton instance = HungrySingleton.getInstance();
HungrySingleton newInstance = (HungrySingleton) constructor.newInstance();
System.out.println(instance);
System.out.println(newInstance);
System.out.println(instance == newInstance);
}
}
对于饿汉式,由于是在类加载的时候就实例化对象了,因此要解决反射攻击问题,可以在构造器内部加一个判断用来防御,这样当反射调用构造器的时候hungrySingleton已经存在,不会再进行实例化并抛出异常:
public class HungrySingleton implements Serializable {
//...
private HungrySingleton(){
if(hungrySingleton != null){
throw new RuntimeException("单例构造器禁止反射调用");
}
}
//...
}
而对于懒汉式,即使加上了上面的防御代码,依然可以通过调整顺序即先使用反射创建实例,再调用getInstance()
创建实例来得到不止一个该类的对象。
枚举实现
枚举类是实现单例的最佳方式,其在多次序列化再进行反序列化之后不会得到多个实例,也可以防御反射攻击。这部分的处理是由ObjectInputStream
和Constructor
这两个类实现的。
public enum EnumInstance {
INSTANCE;
private Object data;
public Object getData() {
return data;
}
public void setData(Object data) {
this.data = data;
}
public static EnumInstance getInstance(){
return INSTANCE;
}
}
容器实现
如果系统中单例对象特别多,则可以考虑使用一个容器把所有单例对象统一管理,但是是线程不安全的。
public class ContainerSingleton {
private static Map<String, Object> singletonMap = new HashMap<String, Object>();
private ContainerSingleton(){
}
public static void putInstance(String key, Object instance){
if(StringUtils.isNotBlank(key) && instance != null){
if(!singletonMap.containsKey(key)){
singletonMap.put(key, instance);
}
}
}
public static Object getInstance(String key){
return singletonMap.get(key);
}
}
Runtime中的应用
查看java.lang
包下的Runtime
类:
public class Runtime {
private static Runtime currentRuntime = new Runtime();
public static Runtime getRuntime() {
return currentRuntime;
}
//...
}
这里的currentRuntime在类加载的时候就实例化好了,属于饿汉式单例模式。
Spring中的应用
查看org.springframework.beans.factory.config
包下的AbstractFactoryBean
:
public abstract class AbstractFactoryBean<T> implements FactoryBean<T>, BeanClassLoaderAware, BeanFactoryAware, InitializingBean, DisposableBean {
//...
public final T getObject() throws Exception {
if (this.isSingleton()) {
return this.initialized ? this.singletonInstance : this.getEarlySingletonInstance();
} else {
return this.createInstance();
}
}
private T getEarlySingletonInstance() throws Exception {
Class<?>[] ifcs = this.getEarlySingletonInterfaces();
if (ifcs == null) {
throw new FactoryBeanNotInitializedException(this.getClass().getName() + " does not support circular references");
} else {
if (this.earlySingletonInstance == null) {
this.earlySingletonInstance = Proxy.newProxyInstance(this.beanClassLoader, ifcs, new AbstractFactoryBean.EarlySingletonInvocationHandler());
}
return this.earlySingletonInstance;
}
}
//...
}
在getObject()
方法中,先判断这个对象是否为单例的,如果不是则直接创建;如果是单例的,那么判断是否被初始化过,如果被初始化了则直接返回,没有的话则调用getEarlySingletonInstance()
方法获取早期的单例对象,如果早期的单例对象不存在,则通过代理来获取。
上一篇: Tomcat 7 RC4 发布
下一篇: Java单例模式