Android 常用代码整理:单例模式的生产环境用法

说明:大部分内容都是参考别的文章,这里做整理是为了以后的编程有实用的模板,可以即需即用。

对于一些类来说,只有一个实例是很重要的。要怎样才能保证一个类只有一个实例并且这个实例易于被访问呢?一个全局变量使得一个对象可以被访问,但它不能防止你实例化多个对象。一个更好的方法是,让类自身负责保存它的唯一实例。这个类可以保证没有其他实例可以被创建(通过截取创建新对象的请求),并且它可以提供一个访问该实例的方法。这就是单例模式(Singleton)。1

常见的几种单例模式:

1、懒汉式(线程不安全)

这种写法 lazy loading 很明显,但在多线程不能正常工作,严格意义上并不算单例模式。

public class Singleton {
    private static Singleton instance;
    
    private Singleton(){ }
    
    public static Singleton getInstance(){
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}
2、懒汉式(线程安全)

这种写法能够在多线程中很好的工作,而且看起来它也具备很好的 lazy loading,但效率很低,99% 情况下不需要同步。
优点:第一次调用才初始化,避免内存浪费。
缺点:必须加锁 synchronized 才能保证单例,但加锁会影响效率。

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

只有在第一次执行此方法时(getInstance()),才真正需要同步。换句话说,一旦设置好 instance 对象,就不再需要同步这个方法了。之后每次调用这个方法,同步都是一种累赘。为了符合大多数 Java 应用程序,很明显地,我们需要确保单例模式能在多线程的状况下正常工作。但是似乎同步 getInstance() 的做法将拖垮性能,而 “双重校验锁” 可以解决这个问题。2

3、饿汉式【推荐使用

这种方式基于 classloder 机制避免了多线程的同步问题,不过,instance 在类装载时就实例化,这时候初始化 instance 显然没有达到 lazy loading 的效果。
描述:这种方式比较常用,但容易产生垃圾对象。
优点:没有加锁,执行效率会提高。
缺点:类加载时就初始化,浪费内存。

public class Singleton {
    private static final Singleton INSTANCE = new Singleton();
    
    private Singleton(){ }
    
    public static Singleton getInstance(){
        return INSTANCE;
    }
}

首先 final 关键词是代表此变量一经赋值,其指向的内存引用地址将不会再改变。声明为 final 的变量,必须在类加载完成时已经赋值,如果你是 final 非 static 成员,必须在构造器、代码块或者在直接定义时赋值;如果是 final static 成员变量,必须在直接定义时或者在静态代码块中赋值。然而,在直接定义时赋值或者在静态代码块中赋值就变成饿汉模式了,所以懒汉模式中,不能用 final 修饰。

4、饿汉式(变种)

表面上看起来差别挺大,其实跟第三种方式差不多,都是在类初始化即实例化 instance。

// 变种1
public class Singleton {
    private static final Singleton instance = null;
    
    static{
        instance = new Singleton();
    }
    
    private Singleton(){ }
    
    public static Singleton getInstance(){
        return instance;
    }
}
// 变种2
public class Singleton {
    public static final Singleton instance = new Singleton();
    private Singleton(){ }
}
5、静态内部类(登记式,既能够实现延迟加载,又能够实现线程安全)【强烈推荐使用

这种方式同样利用了 classloder 的机制来保证初始化 instance 时只有一个线程,它跟第三种和第四种方式不同的是(很细微的差别):第三种和第四种方式是只要 Singleton 类被装载了,那么 instance 就会被实例化(没有达到 lazy loading 效果),而这种方式是 Singleton 类被装载了,instance 不一定被初始化。因为 SingletonHolder 类没有被主动使用,只有显示通过调用 getInstance 方法时,才会显示装载 SingletonHolder 类,从而实例化 instance。想象一下,如果实例化 instance 很消耗资源,想让它延迟加载,另外一方面,不希望在 Singleton 类加载时就实例化,因为不能确保 Singleton 类在其他的地方被主动使用从而被加载,那么这个时候实例化 instance 显然是不合适的。这个时候,这种方式相比第三和第四种方式就显得很合理。

public class Singleton {

    private static class SingletonHolder{
        private static final Singleton INSTANCE = new Singleton();
    }
    
    private Singleton(){ }
    
    // static method declared final.
    public static Singleton getInstance(){
        return SingletonHolder.INSTANCE;
    }
}

(1)相应的基础知识

  • 什么是类级内部类:简单点说,类级内部类指的是,有 static 修饰的成员内部类。如果没有 static 修饰的成员式内部类被称为对象级内部类。
  • 类级内部类相当于其外部类的 static 成分,它的对象与外部类对象间不存在依赖关系,因此可以直接创建。而对象级内部类的实例,是绑定在外部对象实例中的。
  • 类级内部类中,可以定义静态的方法。在静态方法中只能引用外部类中的静态成员方法或变量。
  • 类级内部类相当于其外部类的成员,只有在第一次被使用的时候才会被装载。【内部类(不论是静态内部类还是非静态内部类)都是在第一次使用时才会被加载。 对于非静态内部类是不能出现静态模块(包含静态块,静态属性,静态方法等) 。非静态类的使用需要依赖于外部类的对象。】

(2)多线程缺省同步锁的知识

在多线程开发中,为了解决并发问题,可以通过使用 synchronized 来加互斥锁进行同步控制,但是在某些情况下,JVM 已经隐含的为我们执行了同步,这些情况下就不用我们再来进行同步控制了。这些情况包括:

  • 由静态初始化器(在静态字段上或 static{} 块中的初始化器)初始化数据时
  • 访问 final 字段时
  • 在创建线程之前创建对象时
  • 线程可以看见它将要处理的对象时

(3)对类进行初始化
虚拟机规范严格规定了有且只有四种情况必须立即对类进行初始化:遇到 new、getStatic、putStatic 或 invokeStatic 这4条字节码指令时,如果类没有进行过初始化,则需要先触发其初始化。 生成这4条指令最常见的java代码场景是:

  • 使用 new 关键字实例化对象
  • 读取一个类的静态字段(被 final 修饰、已在编译期把结果放在常量池的静态字段除外)
  • 设置一个类的静态字段(被 final 修饰、已在编译期把结果放在常量池的静态字段除外)
  • 调用一个类的静态方法
6、枚举

这种方式是 Effective Java 作者 Josh Bloch 提倡的方式:

【… xlh …?】这种方法在功能上与公有域方法相近,但它更简洁,无偿地提供了序列化机制,绝对防止多次实例化,即使是在面对复杂的序列化或者反射攻击的时候。虽然这种方法还没有广泛采用,但是单元素的枚举类型已经成为实现 Singleton 的最佳方法。3

它不仅能避免多线程同步问题,而且还能防止反序列化重新创建新的对象,可谓是很坚强的壁垒啊,不过,个人(某博主)认为由于 JDK1.5 中才加入 enum 特性,用这种方式写不免让人感觉生疏,在实际工作中,也很少看见有人这么写过。这种实现方式还没有被广泛采用,但这是实现单例模式的最佳方法。它更简洁,自动支持序列化机制,绝对防止多次实例化。如果涉及到反序列化创建对象时,可以尝试使用第 6 种枚举方式。

public enum Singleton {
    INSTANCE;
    public void whateverMethod(){
	    ...
    }
}
7、双重校验锁

这个是第二种方式的升级版,俗称双重检查锁定。在 JDK1.5 之后,双重检查锁定才能够正常达到单例效果。这种方式采用双锁机制,安全且在多线程情况下能保持高性能。

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

Java volatile 关键字:当一个共享变量被 volatile 修饰时,它会保证修改的值立即被更新到主存;内存可见性,通俗来说就是,线程 A 对一个 volatile 变量的修改,对于其它线程来说是可见的,即线程每次获取 volatile 变量的值都是最新的。

要特别注意使用单例模式时,可能会出现的内存泄漏问题:单例的静态特性使得单例的生命周期和应用的生命周期一样长,如果一个对象已经不再使用了,而单例对象持有对象的引用,那么这个对象将不能被正常回收,这就导致了内存泄漏。

再特别说一句:由于 Dialog 比较特别,只能用 Activity 才能启动(当然有一些用其他 context 也可以,但是要申请权限什么的,不适用于一般app),所以不要使用单例 Dialog 工具类,在 Activity 或 Fragment 的生命中控制 Dialog 的 show 和 dismiss。

【Context】ApplicationActivityServiceContentProviderBroadcastReceiver
Show a DialogNOYESNONONO
Start an ActivityNO1YESNO1NO1NO1
Layout InflationNO2YESNO2NO2NO2
Start a ServiceYESYESYESYESYES
Bind to a ServiceYESYESYESYESNO
Send a BroadcastYESYESYESYESYES
Register BroadcastReceiverYESYESYESYESNO3
Load Resource ValuesYESYESYESYESYES
  • NO1 表示 Application context 的确可以开始一个 Activity,但是它需要创建一个新的 task。这可能会满足一些特定的需求,但是在你的应用中会创建一个不标准的回退栈(back stack),这通常是不推荐的或者不是好的实践。
  • NO2 表示这是非法的,但是这个填充(inflation)的确可以完成,但是是使用所运行的系统默认的主题(theme),而不是你 app 定义的主题。
  • NO3 在 Android4.2 以上,如果 Receiver 是 null 的话(这是用来获取一个 sticky broadcast 的当前值的),这是允许的。

参考文章:
1、http://cantellow.iteye.com/blog/838473
2、http://www.runoob.com/design-pattern/singleton-pattern.html
3、https://blog.csdn.net/qq_33277870/article/details/77967532
4、https://blog.csdn.net/jason0539/article/details/23297037/
5、http://blog.sina.com.cn/s/blog_6d2890600101gb8x.html
6、https://q.cnblogs.com/q/DetailPage/95556/
7、https://blog.csdn.net/qq_32618417/article/details/51703414
8、https://www.jianshu.com/p/c87677f01ed5
9、https://www.jianshu.com/p/413ec659500a
10、https://blog.csdn.net/race604/article/details/9331807


  1. 《设计模式:可复用面向对象软件的基础》 ↩︎

  2. 《Head First 设计模式》 ↩︎

  3. 《Effective Java》 ↩︎

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值