Android—各种单例模式汇总浅析

  • Post author:
  • Post category:其他


声明:本文出自江海博客,转载请注明出处:

https://blog.csdn.net/xueaoandroid/article/details/84558497

网上单例模式的资料多如雪花,之所以写这一篇博文,是想着自己工作学习的总结写出来是属于自己的东西,也加深印象,话不多说提笔就写

本文描述的单例模式有:

  • 饿汉模式
  • 懒汉模式
  • 双重锁单例模式(DCL)
  • 内部类单例模式
  • 枚举单例模式
  • 容器实现单例模式


定义:确保某一个类只有一个实例,并且自行实例化向整个系统提供这个实例

对于系统使用某个类不用区分对象,或者某种类型的对象只应该有且只有一个,一般将这个类设计成单例,是为了避免产生过多的对象而消耗资源,例如:创建的对象消耗的资源过多,如要访问IO和数据库等资源,这时就考虑使用单例



单例模式UML图(图片是网上找的,如有侵权请知会)

图片是网上在这里插入图片描述



单例的实现
  • 构造函数不对外开放,一般为private
  • 通过一个静态方法或者枚举返回对象
  • 确保单例的对象有且只有一个,特别是在多线程环境下
  • 确保单例对象在反序列化时不会重新构建对象


简单实例

例如一个国家只有一个总统或主席,一个公司只有一个老板,一个项目只有一个经理等等,一个总统下有多个部长,部长下有很多机关公职人员,但总统只有一个,如下代码示例:

//公务员
public class CivilServant{
	public void work(){
		//公务员的工作
	}
}
//部长
public class Minister extends CivilServant{
	public void work(){
		//部长的工作
	}
}
//总统
public class President extends CivilServant{
	private static final President president = new President();
	//总统只有一个
	privater President(){
		//构造方法私有化,使得外部程序不同通过构造函数来获取president对象
	}
	//对外暴露一个静态函数,供外部获取一个静态对象
	public static President getPresident(){
		return president;
	}
	public void work(){
		//总统的工作
	}
}


饿汉模式:

文中president 在声明对象时就已经初始化,一般将这种实现模式称为饿汉模式;

注意:相比其它模式饿汉模式在声明对象时有个final关键字;



懒汉模式:

再看下面一种实现模式

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

这里给getInstance()方法加了synchronized关键字,变成了一个同步方法,保证了在多线程情况下保证了单例对象唯一性;

大家细想会知道,这种情况下即使instance已经被初始化,每次调用getInstance()方法都会被同步,这样也会消耗不必要的资源,这也是懒汉模式存在的最大问题;



DCL模式:

项目中一般常见使用的是DCL单例模式(Double Check Lock)

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

可以看到getInstance()方法对ourInstance进行两次判空,第一次判断是为了不必要的同步,第二次判断是在为null情况下创建实例,

注意: outInstance在声明时使用了关键字:volatile

我们来分析:假设线程A执行到ourInstance = new Singleton();这看起来是一句代码,但实际上它并不是一个最小执行单位,这条代码最终会被编译成多条汇编指令,它大致做了以下三件事:

  1. 给Singleton的实例分配内存
  2. 调用Singleton()构造函数,初始化成员
  3. 将ourInstance对象指向分配内存(此时ourInstance就不再是null了)

由于java编译器允许处理器乱序执行,上面第二,第三步执行顺序是无法确定的,就是说顺序可能是1-2-3,也可能是1-3-2,如果是后再,在3执行完毕2未执行之前,被切换到B线程,这时因为ourInstance在A线程中已经执行了3,ourInstanc已经是非空,所以线程B直接取走ourInstance,再使用时就会报错;因此SUN官方增加了volatile关键字,可以保证ourInstance对象每次都是从主内存读取;当然volatile多少会影响点性能,但考虑到程序正确性,牺牲点性能也是可以接受的;



静态内部类单例模式

DCL虽然在一定程度上解决了资源消耗,多余的同步锁,线程安全等问题,但它在某种程度上还是会出现失效,这个问题被称为双重检查锁定(DCL)失效,在《java并发编程实践》中谈到这个问题,并指出这种“优化”是丑陋,不建议使用,建议使用静态内部类单例模式

public class Singleton {

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

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



枚举单例模式

最简单的单例实现

枚举在java中与普通类是一样的,不仅能够有,还能够有自己的方法,最重要的是默认枚举实例的创建时线程安全的,并且在任何情况下它都是一个单例

   public enum SingletonEnum{
   		INSTANCE;
   }

我们知道通过序列化可以将一个单例的实例对象写到磁盘,然后再读出来,从而有效地获取一个实例,即使构造函数是私有的,反序列化时依然可以通过特殊的途径去创建类的一个新的实例,相当于调用该类的构造函数,反序列化操作提供了一个特殊的钩子函数,类中具有一个私有的readResolve()函数,它可以控制对象的反序列化,

如下代码,如果要杜绝单例对象在反序列化时重新生成对象,必须加入readResolve()函数,并且readResolve()函数将单例对象返回,而不是重新生成一个新的对象;对于枚举并不存在这个问题,因为即使反序列化它也不会生成新的实例;



容器单例模式

单例模式的另一种实现

public class SingletonManager {

 private static Map<String, Object> mMap = new HashMap<>();

    private SingletonManager(){

    }
    public static void registerService(String key, Object instance) {
        if (!mMap.containsKey(key)) {
            mMap.put(key, instance);
        }
    }

    public static Object getService(String key) {
        return mMap.get(key);
    }
 }

程序中是将多种类型注入到统一的管理类中,在使用时根据key获取对应的单例对象,这也使得我们可以管理多种类型单例,并且在使用时可以通过统一的接口进行获取操作,降低了用户使用成本,也隐藏了具体实现,降低了耦合度;

工作中实现哪种单例取决于项目本身,不管以哪种模式实现单例,它们的核心原理都是将构造函数私有化,并且通过静态方法获取一个唯一的实例,在这个获取的过程中必须保证线程安全,防止反序列化导致重新生成实例对象等问题,

注意:单例对象持有Context,那么很容易引发内存泄露,这时传递给单例对象的Context最好是Application Context



版权声明:本文为xueaoandroid原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。