阅读 87

Android备忘录《单例模式》

什么是单例:单例模式能够保证整个应用中有且只有一个实例,并且只能够通过其暴露的方法获取实例,不能够自由构造。

解决的问题是:可以保证一个类在内存中的对象的唯一性,在一些常用的工具类、线程池、缓存,数据库,账户登录系统、配置文件等程序中可能只允许我们创建一个对象,一方面如果创建多个对象可能引起程序的错误,另一方面创建多个对象也造成资源的浪费。

1、饿汉式(占资源少的情况下推荐使用)

public class SingleTon {

    private static SingleTon instance = new SingleTon();
    
    private SingleTon(){
        
    }
    
    public static SingleTon getInstance(){
        return instance;
    }
    
}
复制代码

适合那些在初始化时就要用到单例的情况,如果单例对象初始化非常快,而且占用内存非常小的时候这种方式是比较合适的。如果单例初始化的操作耗时比较长而应用对于启动速度又有要求,或者单例的占用内存比较大,再或者单例只是在某个特定场景的情况下才会被使用,而一般情况下是不会使用时,使用「饿汉式」的单例模式就是不合适的,这时候就需要用到「懒汉式」的方式去按需延迟加载单例

优点:类加载的时候就完成了实例化,避免了线程的同步问题,获取单例速度快。

缺点:由于在类加载的时候就实例化了,所以没有达到LazyLoading(懒加载)的效果,如果没有用到这个实例它也会加载,会造成内存的浪费(但是这个浪费可以忽略)。

2、懒汉式(线程不安全,不推荐使用)

public class SingleTon {

    private static SingleTon instance = null;

    private SingleTon(){

    }

    public static SingleTon getInstance(){
        if(instance == null){
            instance = new SingleTon();
        }
        return instance;
    }

}
复制代码

「懒汉式」与「饿汉式」的最大区别就是将单例的初始化操作,延迟到需要的时候才进行,这样做在某些场合中有很大用处。比如某个单例用的次数不是很多,但是这个单例提供的功能又非常复杂,而且加载和初始化要消耗大量的资源,这个时候使用「懒汉式」就是非常不错的选择。

优点:延迟加载,不浪费资源。 缺点:是第一次加载时需要及时进行实例化,这种写法在多线程模式下存在安全问题 (有多个线程去调用getInstance()方法来获取Singleton的实例,那么就有可能发生这样一种情况当第一个线程在执行if(instance==null)这个语句时,此时instance是为null的进入语句。在还没有执行instance=new Singleton()时(此时instance是为null的)第二个线程也进入if(instance==null)这个语句,因为之前进入这个语句的线程中还没有执行instance=new Singleton(),所以它会执行instance=newSingleton()来实例化Singleton对象,因为第二个线程也进入了if语句所以它也会实例化Singleton对象。这样就导致了实例化了两个Singleton对象。)

3、懒汉式(线程安全,效率低,不推荐使用)

public class SingleTon {

    private static SingleTon instance = null;

    private SingleTon() {

    }

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

}
复制代码

之前提到懒汉式的单例写法会有线程安全问题,这种方式通过增加同步锁方式,来解决线程安全问题,但是新的问题会存在,就是在频繁调用获取单例时,会频发检查同步,而此时会比较耗时,导致效率低下。

4、式懒汉式双重校验锁(推荐使用)

public class SingleTon {

    private static SingleTon instance = null;

    private SingleTon() {

    }

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

}
复制代码

DCL 在getInstance 方法中 对instance 进行两次判空:为什么要判断两次,第一个判空是为了避免不必要的同步,第二层判断是为了在null 情况下创建实例。instance=new Singleton(); 语句看起来是有代码,单实际是一个原子操作,最终会被编译成多条汇编指令,大致做了三件事:

1.给Singleton 分配内存

2.调用Singleton 的构造函数,初始化成员字段

3.将instance 对象指向分配的内存空间(此时instance 就不是null 了)

但是jdk 1.5 以后java 编译器允许乱序执行 。所以执行顺序可能是1-3-2 或者 1-2-3.如果是前者先执行3 的话 切换到其他线程,instance 此时 已经是非空了,此线程就会直接取走instance ,直接使用,这样就回出错。DCL 失效。解决方法 SUN 官方已经给我们了。

将instance 定义成 private volatile static Singleton instance =null

DCL 的优点,资源利用率高,第一次执行getInstance 时才会被实例化,效率高。

缺点:第一次加载反应慢,也由于java 内存 模型的原因偶尔会失败,在高并发环境下,有一定缺陷,虽然发生概率很小。(很常用)

private static volatile Singleton instance = null; 这里使用了volatile关键字,因为多个线程并发时初始化成员变量和对象实例化顺序可能会被打乱,这样就出错了,volatile可以禁止指令重排序。双重校验虽然在一定程度解决了资源的消耗和多余的同步,线程安全问题,但在某些情况还是会出现双重校验失效问题,即DCL失效,使用volatile可保证每次都从主内存中读取,可能会导致性能问题。

5、静态内部类实现单例

public class Singleton { 
	private Singleton() { 
	
	} 
	private static class SingletonHolder { 
		private static final Singleton singleton = new Singleton(); 
	} 
	public static Singleton getInstance() { 
		return SingletonHolder.singleton; 
	}
}
复制代码

这种方式跟饿汉式方式采用的机制类似,但又有不同。两者都是采用了类装载的机制来保证初始化实例时只有一个线程。不同的地方在饿汉式方式是只要Singleton类被装载就会实例化,没有Lazy-Loading的作用,而静态内部类方式在Singleton类被装载时并不会立即实例化,而是在需要实例化时,调用getInstance方法,才会装载SingletonHolder类,从而完成Singleton的实例化。类的静态属性只会在第一次加载类的时候初始化,所以在这里,JVM帮助我们保证了线程的安全性,在类进行初始化时,别的线程是无法进入的。

优点:避免了线程不安全,延迟加载,效率高。

6、枚举单例

public enum SingletonEnum {  
      
     instance;   
       
     private SingletonEnum() {}  
       
     public void method(){
     
     }  
}  
复制代码

可以看到枚举的书写非常简单,访问也很简单SingletonEnum.instance.method();
更加简洁,线程安全,还能防止反序列化导致重新创建新的对象,而以上方法还需提供readResolve方法,防止反序列化一个序列化的实例时,会创建一个新的实例。枚举单例模式,我们可能使用的不是很多,但《Effective Java》一书推荐此方法,说“单元素的枚举类型已经成为实现Singleton的最佳方法”。不过Android使用enum之后的dex大小增加很多,运行时还会产生额外的内存占用,因此官方强烈建议不要在Android程序里面使用到enum,枚举单例缺点也很明显。

7、容器单例

public class SingletonManager { 
    private SingletonManager() { } 

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

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

    public static Object getInstance(String key) {
        return instanceMap.get(key); 
    }
}


    public class SingletonPattern { 
        SingletonPattern() { } 
        public void doSomething() { 
            Log.d("wxl", "doSomeing"); 
        } 
    }
复制代码

代码调用:

SingletonManager.registerInstance("SingletonPattern", new SingletonPattern());
SingletonPattern singletonPattern = (SingletonPattern) SingletonManager.getInstance("SingletonPattern");
singletonPattern.doSomething();
复制代码

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

文章分类
Android