声明:本文出自江海博客,转载请注明出处:https://blog.csdn.net/xueaoandroid/article/details/84558497
网上单例模式的资料多如雪花,之所以写这一篇博文,是想着自己工作学习的总结写出来是属于自己的东西,也加深印象,话不多说提笔就写
本文描述的单例模式有:
对于系统使用某个类不用区分对象,或者某种类型的对象只应该有且只有一个,一般将这个类设计成单例,是为了避免产生过多的对象而消耗资源,例如:创建的对象消耗的资源过多,如要访问IO和数据库等资源,这时就考虑使用单例
例如一个国家只有一个总统或主席,一个公司只有一个老板,一个项目只有一个经理等等,一个总统下有多个部长,部长下有很多机关公职人员,但总统只有一个,如下代码示例:
//公务员
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单例模式(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();这看起来是一句代码,但实际上它并不是一个最小执行单位,这条代码最终会被编译成多条汇编指令,它大致做了以下三件事:
由于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 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