一、什么是单例模式?
单例模式 就是 确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例。在应用单例模式时,单例对象的类必须保证只有一个实例存在。许多时候整个系统只需要拥有一个全局对象,这样有利于我们协调系统整体的行为。
二、单例模式的使用场景
不能自由构造对象的情况,确保某个类有且只有一个对象的场景,避免产生多个对象消耗过多的资源,或者某种类型的对象只应该有且只有一个。例如,创建一个对象需要消耗的资源过多,如要访问 IO 和数据库等资源,这是就要考虑使用单例模式。
三、实现单例模式的关键点
1. 构造函数不对外开放,一般为 Private;
通过将单例类的构造函数私有化,使得客户端代码不能通过 new 的形式手动构造单例类的对象。
2. 通过一个静态方法或者枚举返回单例类对象;
单例类会暴露一个公有静态方法,客户端需要调用这个静态方法获取到单例类的唯一对象。
3. 确保单例类对象有且只有一个,尤其是在多线程环境下;
在获取这个单例对象的过程中需要确保线程安全,即在多线程环境下构造单例类的对象也是有且只有一个。(实现较困难)
4.确保单例类对象在反序列化时不会重新构建对象。
四、单例模式的几种实现方式
1. 饿汉模式
public class Singleton{
private static final Singleton mInstance = new Singleton();
// 构造函数私有
private Singleton(){
}
// 公有的静态函数,对外暴露获取单例对象的接口
public static Singleton getInstance(){
return mInstance;
}
}
饿汉模式是在声明一个静态对象时就对其初始化。
- 缺点:反序列化时会出现重新创建对象的情况。
2. 懒汉模式
public class Singleton{
private static Singleton mInstance;
private Singlrton(){
}
public static synchronized Singleton getInstance(){
if(mInstance == null){
mInstance = new Singleton();
}
return mInstance;
}
}
懒汉模式是声明一个静态对象,并且在用户第一次调用 getInstance 时进行初始化。
- 优点:单例只有在使用时才会被实例化,在一定程度上节约了资源;
- 缺点:第一次加载时需要及时进行实例化,反应慢;反序列化时会出现重新创建对象的情况;
- 最大的问题:getInstance()方法中添加了 synchronized 关键字,于是乎,每次调用getInstance()都进行同步,造成不必要的同步开销;
- 结论:懒汉模式一般不建议使用。
3.Double CheckLock(DCL)实现单例(推荐使用)
public class Singleton(){
private volatile static Singleton mInstance = null;
private Singleton(){
}
public void doSomething(){
System.out.println("Do sth.");
}
public static Singleton getInstance(){
if(mInstance == null){
synchronized(Singleton.class){
if(mInstance == null){
mInstance = new SIngleton();
}
}
}
return mInstance;
}
}
其中,加上 volatile 关键字后,可以解决 DCL 失效问题,但或多或少会影响到性能,但考虑到程序的正确性,牺牲这点性能还是值得的。
- 优点:既能够在需要时才初始化单例,并能在绝大多数场景下保证单例对象的唯一性,又能够保证线程安全,且单例对象初始化后调用 getInstance 不进行同步锁。资源利用率高,第一次执行 getInstance 时单例对象才会被实例化,效率高;
- 缺点:第一次加载时反应稍慢,也由于 Java 内存模型的原因偶尔会失败。在高并发环境下也有一定的缺陷,虽然发生概率很小;反序列化时会出现重新创建对象的情况;
- 结论:除了 并发场景比较复杂 或者 低于 JDK 6 版本 的情况,DCL模式是使用最多的单例实现方式。
4.静态内部类单例模式(推荐使用)
public class Singleton(){
private Singleton(){
}
public static Singleton getInstance(){
return SingletonHolder.mInstance;
}
/**
* 静态内部类
*/
private static class SingletonHolder(){
private static final Singleton mInstance = new Singleton();
}
}
虽然 DCL 在一定程度上解决了资源消耗、多余同步、线程安全等问题,但它在某些情况下会出现 双重检查锁定(DCL)失效 问题,所以,建议用 静态内部类单例模式 代替。
- 优点:能够保证线程安全,能够保证单例对象的唯一性,延迟了单例的实例化;
- 缺点:反序列化时会出现重新创建对象的情况;
- 结论:推荐使用该方式实现单例模式。
5.枚举单例
public enum SingletonEnum{
INSTANCE;
public void doSomething(){
System.out.println("Do sth.");
}
}
- 优点:写法简单;默认枚举实例的创建是线程安全的;任何情况下它都是一个单例,即使是反序列化时;
6.使用容器实现单例模式
public class SingletonManager{
private static Map<String,Object> objMap = new HashMap<String,Object>();
private Singleton(){
}
public static void registerService(String key,Object mInstance){
if(!objMap.containKey(key)){
objMap.put(key,mInstance);
}
}
public static ObjectgetService(String key){
return objMap.get(key);
}
}
- 优点:降低用户的使用成本,对用户隐藏了具体实现,降低了耦合度。
五、总结
核心原理:将构造函数私有化,且通过静态方法获取一个唯一的实例,在此获取过程中必须保证线程安全、防止反序列化导致重新生成实例对象等问题。
优点:
(1)、由于单例模式在内存中只有一个实例,减少了内存开支,特别是一个对象需要频繁的创建、销毁时,而且创建或销毁时性能又无法优化,单例模式的优势就非常明显;
(2)、由于单例模式只生成一个实例,所以,减少了系统的性能开销,当一个对象的产生需要较多资源时,如读取配置、生产其他依赖对象时,则可以通过在应用启动时直接产生一个单例对象,然后永久驻留内存的方式来解决;
(3)、单例模式可以避免对资源的多重占用,例如一个写文件操作,由于只有一个实例存在内存中,避免对同一个资源文件的同时写操作;
(4)、单例模式可以在系统设置全局的访问点,优化和共享资源访问,例如,可以设计一个单例类,负责所有数据表的映射处理。缺点:
(1)、单例模式一般没有接口,扩展很困难,除了修改代码;
(2)、单例对象如果持有 Context ,那么很容易引发内存泄漏,此时需要注意传递给单例对象的 Context 最好是 Application Context 。扩展:
Q:如何在上述几个示例中杜绝单例对象在被反序列化时重新生成对象?
A:通过序列化可以将一个单例的实例对象写到磁盘,然后再读回来,从而有效地获得一个实例。即使构造函数私有的,反序列化时已然可以通过特殊的途径去创建类的一个新的实例,相当于调用该类的构造函数。反序列化操作提供了一个很特别的钩子函数,类中具有一个私有的、被实例化的方法 readResolve(),这个方法可以让开发人员控制对象的反序列化。
可在如上几个示例中加入如下方法来杜绝单例对象在被反序列化时重新生成对象:
private Object readResolve() throws ObjectStreamException{
return mInstance;
}
即在 readResolve() 方法中将 mInstance 对象返回,而不是默认的重新生成一个新的对象。