设计模式-单例模式

时间:2022-01-27 20:33:02

1、介绍

在开发过程中,发现有时整个系统只需要拥有一个全局对象,这就可以应用单例模式。例如:在应用中,应该只有一个ImageLoader实例,这个ImageLoader中含有线程池、缓存系统、网络请求等,构造多个实例资源消耗多。

2、使用场景

Ensure a class has only one instance,and provide a global of access to it.

意思是: 确保一个类只有一个实例,而且自行实例化并向整个系统提供这个实例。

单例模式的主要作用是确保一个类只有一个实例存在。单例模式可以用在建立目录、数据录连接等单线程操作的场合,用于对系统资源的控制。

3、UML类图

UML类图如下所示:

设计模式-单例模式

角色介绍:

  1. Client: 高层客户端
  2. Singleton:单例类

关键点:

  1. 构造函数不对外开放,一般修饰符为private
  2. 通过一个静态方法或者枚举返回单例类对象
  3. 确保单例类的对象有且只有一个,尤其是在多线程环境下
  4. 确保到单例类对象在反序列化时不会重新构建对象

因为单例类的构造函数私有化,从而客户端代码不能通过new 的形式手动构造对象。单例类会暴露一个公有静态方法,外界通过这个静态方法获取单例类对象,在获取单例类对象的过程中需要确保线程安全。

4、示例

1、饿汉模式

public class Singleton {
	private static Singleton instance = new Singleton();
	private Singleton() {}
	public static synchronized Singleton getInstance() {
		return instance;
	}
}
Singleton类加载时,就进行了对象实例化。

2、懒汉模式

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

  懒汉模式实在第一次引用类时,才进行对象实例化。getInstance代码中添加了synchronized,是在多线程情况下保证单例对象唯一的一种手段。但是,细心的人肯定会发现,若是instance已经初始化,每次调用getInstance 都会进行同步,这样会消耗资源。

3、双重检查锁定(DCL)

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

 双重检查锁定是优化方案,既能保证线程安全,又能在单例对象初始化后调用getInstance不进行同步锁。getInstance方法中对instance进行了两次判断:第一层判断主要是为了避免不必要的同步,第二层判断是为了在null的情况下创建实例。

  4、静态内部类

DCL在《Java并发编程实践》谈到某些情况下失效问题,并指出DCl这种“优化”是丑陋的,不赞成使用,推荐以下代码替代:

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

当第一次加载Singleton类时并不会初始化instance,只有在第一次调用getInstance才会导致instance初始化,这时才会导致虚拟机加载SingletonHolder类,这种方式不仅能够确保线程安全,也能保证单例对象唯一性,同时也延迟了单例的初始化,推荐使用该方法。

5、枚举单例

public enum Singleton {
	INSTANCE;
	public void doSomething() {
		System.out.print("do something");
	}
}

写法简单是枚举单例最大的优势,枚举在java中与普通类是一样的,不仅有字段,还有自己的方法,最重要的是默认枚举实例的创建是线程安全的,并且在任何情况下都是一个单例。

6、使用容器实现单例模式

import java.util.HashMap;
import java.util.Map;
public class Singleton {
	private static Map<String, Object> objMap = new HashMap<>();
	private Singleton () {}
	public static void registerService(String key, Object instance) {
		if (!objMap.containsKey(key)) {
			objMap.put(key, instance);
		}
	}
	public static Object getInstance(String key) {
		return objMap.get(key);
	}
}
在程序初始化,将多种单例类型注入到一个统一的管理类中,在使用时根据key获取对象对应类型的对象。这种方式使得我们可以管理多种类型的单例,并且在使用时可以通过统一的接口进行获取操作,降低使用成本,也隐藏具体实现,降低了耦合度。

5、总结

优点:

  1. 由于单例模式在内存中只有一个实例,减少了内存开支,特别是一个对象需要频繁地创建、销毁时,而且创建和销毁时性能又无法进行优化,单例模式的优势非常明显。
  2. 由于单例模式只生成一个实例,减少了系统的性能开销,当一个对象的产生需要较多的资源时,如:读取配置,产生其他依赖对象时,可以通过在应用启动时直接产生一个单例对象,用永久驻留内存方式解决。
  3. 单例模式可以避免对资源的多重占用,例如一个写文件操作,由于只有一个实例存在内存中,避免同一个资源文件的同时写操作。
  4. 单例模式可以在系统设置全局的访问点,优化和共享资源访问,例如:可以设计一个单例类负责所有数据表的映射处理。

缺点:

  1. 单例模式一般没有接口,扩展性差。
  2. 单例模式对测试不利。在并行开发环境中,若是采用单例模式的类没有完成,是不能进行测试的;单例模式的类通常不会实现接口,妨碍了使用mock方式虚拟一个对象。
  3. 单例模式与单一职责原则有冲突。一个类应该只实现一个逻辑,而不关心它是否是单例的,是不是要用单例模式取决于环境,单例模式把“要单例”和业务逻辑融合在一起。
  4. 在Android中,单例对象若是持有Context,那么容易引发内存泄露,此时需要注意传递给单例对象的Context最好是Application Context。