Android设计模式—单例模式

1.单例模式
单例模式是一种创建型设计模式。它保证一个类只有一个实例,并且这个单例类提供一个函数接口让其他类获取到这个唯一的实例。

单例四大原则:
①构造私有。构造函数不对外开放,一般为private。
②以静态方法或枚举返回实例。
③确保实例只有一个,尤其是在多线程环境。
④确保实例在反序列化时不会重新构建对象。

使用单例模式的目的:
当一个对象被很多地方调用时,比如网络请求库okhttp、Retrofit就可以只实例化一次,不用频繁的进行创建和销毁,从而节省系统资源。

单例模式应用场景:
①频繁访问数据库或文件的对象;
②工具类对象;
③创建对象时耗时过多或耗费资源过多,但又经常用到的对象;
Android中习惯使用单例的常见类: xxxManager , xxxHelper , xxxUtils 等

单例模式优点:
①单例类只有一个实例,节省内存开销,对于一些需要频繁创建、销毁的对象,使用单例模式可以提高系统性能;
②避免对资源的多重占用。例如一个文件操作,由于只有一个实例存在内存中,避免对同一资源文件的同时操作。
③单例模式可以在系统设置全局的访问点,优化和共享数据,例如页面计数器就可以用单例模式实现计数值的保存。

单例模式缺点:
①获取对象时不能用new
②单例对象如果持有Context,则很容易引发内存泄露。
③扩展性差。单例模式一般没有接口,扩展很困难,若要扩展,只能修改代码来实现。

2.单例模式的创建方式
单例的实现主要是通过两个步骤:
①将该类的构造方法定义为私有,这样其他处的代码就无法通过调用该类的构造方法来实例化该类的对象,只有通过该类提供的静态方法来得到该类的唯一实例。
②在该类内提供一个静态方法,当调用这个方法时,如果类持有的引用不为空就返回这个引用,如果类持有的引用为空就创建该类的实例并将实例的引用赋予该类保持的引用。

单例模式的创建方式有五种,主要看是在单线程中运行还是在多线程中运行。
(1)饿汉式 (在声明的时候已经初始化了)
public class Singleton {
private final static Singleton s = new Singleton();
private Singleton(){
}
public static Singleton getInstance(){
return s;
}
}
①构造函数使用private修饰,保证外部无法访问;
②在声明对象时初始化;
③static关键字修饰静态变量,使内存中只有一份数据;
④final关键字保证只初始化一次;
饿汉式在类加载时就完成了实例化,避免了多线程的同步问题。
优点:避免了多线程的同步问题;获取实例的速度快。
缺点:类加载比较慢,没有达到懒加载的效果,如果一直未使用过该实例,就造成了内存浪费。

(2)懒汉式
懒汉式分两种,一种是普通的懒汉式(线程不安全),一种是同步方法的懒汉式(线程安全),。
①普通的懒汉式(线程不安全)
public class Singleton {
private static Singleton instance = null;
private Singleton() {
}
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
①构造方法使用private修饰,外部无法访问;
②使用的时候要调用getInstance()才会初始化;
③static关键字修饰,静态变量在内存中只有一份数据;
这是懒汉式中最简单的一种写法,只有在方法第一次被访问时才会实例化,达到了懒加载的效果。
但是这种写法有个致命的问题,就是多线程的安全问题。假设对象还没被实例化,然后有两个线程同时访问,那么就可能出现多次实例化的结果,所以这种写法不可采用。
②同步方法的懒汉式(线程安全)
public class Singleton {
private static Singleton instance = null;
private Singleton() {
}
public static synchronized Singleton getInstance() {
if (instance ==null) {
instance = new Singleton();
}
return instance;
}
}
这种写法对getInstance()方法进行了加锁处理,保证同一时刻只能有一个线程访问并获得实例,但是缺点也很明显,因为synchronized是修饰整个方法,每个线程访问都要进行同步,而其实这个方法只执行一次实例化代码就够了,每次都同步方法显然效率低下。为了改进这种写法,就有了下面的双重检查加锁式。

(3)双重检查加锁式DCL(double check lock)线程安全
public class Singleton {
private static volatile Singleton instance;
private Singleton() {}
public static Singleton getInstance() {
if (instance ==null) {
synchronized (Singleton.class) {
if (instance ==null) {
instance = new Singleton();
}
}
}
return instance;
}
}
①构造函数使用private修饰,外部无法访问;
②在使用的时候调用getInstance()进行初始化;
③static关键字修饰,静态变量在内存中只有一份数据;
④synchronized线程安全,保证多线程情况下的唯一性;
⑤两次判空避免多次同步;
这里用了两个if判断,也就是Double-Check,并且同步的不是方法,而是代码块,效率较高,是对上一种写法的改进。第一层判空:避免不必要的同步;第二次判空:在instance为null的情况下创建实例。
做两次判断是为了线程安全考虑。还是那个场景,对象还没实例化,两个线程A和B同时访问静态方法并同时运行到第一个if判断语句,这时线程A先进入同步代码块中实例化对象,结束之后线程B也进入同步代码块,如果没有第二个if判断语句,那么线程B也同样会执行实例化对象的操作了。
注意:在双重检查加锁式中,声明实例的时候要用到volatile关键字—private static volatile Singleton instance,这是因为在java虚拟机中指令是可以重排序的,所以getInstance()方法中的指令就不一定按照写的代码顺序执行,这样就可能会导致DCL失效。加上volatile关键字可以禁止虚拟机重排序。
具体解释一下:问题出在这行简单的赋值语句:instance = new Singleton();它并不是一个原子操作,事实上它可以抽象为下面几条JVM指令:
memory = allocate();//1.分配对象的内存空间
initInstance(memory);//2.初始化对象
instance = memory;//3.设置instance指向刚分配的内存地址
上面操作2依赖于操作1,但是操作3并不依赖于操作2,所以JVM可以以“优化”为目的对它们进行重排序,经过重排序后如下:
memory = allocate();//1.分配对象的内存空间instance = memory;//3.设置instance指向刚分配的内存地址
initInstance(memory);//2.初始化对象
可指令重排序后,操作3排在了操作2之前,即引用instance指向内存memory时,这段崭新的内存还没有初始化,引用instance指向了一个“被部分初始化的对象”。此时如果另一个线程调用getInstance方法,由于instance已经指向了一块内存空间,从而if条件判断为false(也就是instance!=null),方法返回instance引用,用户得到了没有完成初始化的“半个”单例。也就是说在只有DCL没有volatile的懒加载单例模式中,仍然存在着并发陷阱,这里确实不会拿到两个不同的单例了,但是却拿到了“半个”单例(没有完成初始化)。

(4)静态内部类方式
public class Singleton {
private Singleton() {
}
public static Singleton getInstance() {
return SingletonHelper.INSTANCE;
}
private static class SingletonHelper {
private static final Singleton INSTANCE = new Singleton();
}
}
①构造函数使用private修饰,外部无法访问;
②使用的时候调用getInstance()才会初始化;
③调用getInstance()才去加载静态内部类,确保线程安全;
这种方式跟饿汉式有点类似,但又不同,它做到了延迟加载。两者都是采用了类加载的机制来保证初始化实例时只有一个线程。不同处在于饿汉式只要Singleton类被装载就会实例化,没有懒加载的效果,而静态内部类方式在Singleton类被装载时并不会立即实例化,而是在需要实例化时调用getInstance()方法才会装载SingletonHelper类,从而完成Singleton的实例化。
静态内部类的优点是:外部类加载时并不需要立即加载内部类,内部类不被加载则不去初始化INSTANCE,故不占内存。即当Singleton第一次被加载时并不需要去加载SingletonHelper,只有当getInstance()方法第一次被调用时才会去初始化INSTANCE,由Java虚拟机来保证其线程安全性,确保该成员变量只能被初始化一次。由于getInstance()方法没有任何线程锁定,因此其性能不会造成任何影响。
同时,因为类的静态属性只会在第一次加载类的时候初始化,也就保证了SingletonInstance中的对象只会被实例化一次,并且这个过程也是线程安全的。

(5)枚举方式
public class Singleton {
private Singleton(){ //私有构造函数
}
//定义一个静态枚举类
static enum SingletonEnum{
INSTANCE; //创建一个枚举对象
private Singleton singleton;
//私有化枚举的构造函数
private SingletonEnum(){
singleton = new Singleton();
}
public Singleton getInstance(){
return singleton;
}
}
//对外暴露一个获取Singleton对象的静态方法
public static Singleton getInstance(){
return SingletonEnum.INSTANCE.getInstance();
}
}
使用枚举实现单例模式,实现简单调用效率高。枚举本身就是单例,由JVM从根本上提供保障,避免反射和反序列化的漏洞,缺点时没有延迟加载。
注:枚举和java普通类是一样的,不仅能够拥有字段还能够有自己的方法。最重要的是默认枚举实例的创建是线程安全的,并且在任何情况下都是一个单例。而且枚举不存在反序列化的问题,即使是反序列化也不会重新创建实例。

3.Android系统下的单例模式:
//获取WindowManager服务引用
WindowManager wm = (WindowManager) getSystemService(getApplication().WINDOW_SERVICE);

4.单例可能引发内存泄露
单例模式使用不恰当会造成内存泄漏。因为单例的静态特性使得单例的生命周期和应用的生命周期一样长, 如果一个对象已经不需要使用了,但是单例对象还持有该对象的引用,那么这个对象就不能被正常回收,因此会导致内存泄漏。

举个例子:
①新建一个工程。
②配置好LeakCanary检测环境。
③添加一个单例类AppManager
在这里插入图片描述
④在MainActivity中使用此单例
在这里插入图片描述
运行代码后做如下操作:
①点击返回键,退出MainActivity。
②等待10秒。
做完如上操作后,LeakCanary提示MainActivity内存泄漏:
在这里插入图片描述
现在来分析一下内存泄漏的原因:
AppManager appManager = AppManager.getInstance(this);这句传入的是Activity的Context,由于Activty是间接继承自Context的。当这个Activity退出时Activity应该被回收, 但是单例中又持有它的引用,导致Activity回收失败,造成内存泄漏。

为了防止误传Activity的Context , 可以修改一下单例的代码:
在这里插入图片描述
这样不管外面传入什么Context,最终都会使用Applicaton的Context,而单例的生命周期和应用的一样长,这样就防止了内存泄漏。

修改完毕后,运行代码,重复以上操作,将会发现leakCanary没有检测出泄漏。

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值