前言
继上一篇Android常用设计模式之工厂模式,今天给大家讲解一篇Android常用的设计模式——单例模式。我想单例模式应该是最常用的模式之一,可能很多老铁认为单例模式已经是熟悉的不行了,但是我还是要写一篇,作为记录。
设计模式之单例模式
单例模式
说起单例模式,我想大家可能都清楚,通常我们的APP的一个类,在运行的时候可能有很多个对象,但是我们的单例模式不一样,在我们的类中只存在一个实例对象。
那么在我们Android中经常在哪些场景会使用到我们的单例模式呢?
数据库连接、线程池、配置文件解析加载等一些非常耗时,占用系统资源的操作,并且还存在频繁创建和销毁对象,如果每次都创建一个实例,这个系统开销是非常恐怖的,所以,我们可以始终使用一个公共的实例,以节约系统开销。
- 静态成员变量
- 私有构造方法
- 全局访问
单例模式的分类
- 饿汉模式
public class Singleton {
private static Singleton instance = new Singleton();
private Singleton(){}
public static Singleton getInstance(){
return instance;
}
}
复制代码
这种模式是只要类一加载,那么我们就创建了对象
我们测试一下:
public class SingletonTest {
@Test
public void getInstance(){
Singleton s1 = Singleton.getInstance();
Singleton s2 = Singleton.getInstance();
System.out.println("对象1:" + s1.hashCode());
System.out.println("对象2:" + s2.hashCode());
if (s1 == s2) {
System.out.println("对象相等");
} else {
System.out.println("对象不等");
}
}
}
复制代码
此时,我们调用两次 Singleton 类的 getInstance() 方法来获取 Singleton 的实例。我们发现 s1 和 s2 是同一个对象。
- 懒汉模式
懒汉模式,是一种延迟加载。在我们创建类的时候是不会立马创建对象,只有在我们需要的时候才会加载。
public class Singleton2 {
private Singleton2(){}
private static Singleton2 instance = null;
public static Singleton2 getInstance(){
if(instance == null){
instance = new Singleton2();
}
return instance;
}
}
复制代码
这种模式有个缺点就是线程并不安全,当两个线程同时调用的时候,线程1调用了getInstance()的时候,instance并没有创建完成,这时线程2又在调用getInstance(),又会从新创建一次对象,此时就不能保证我们的对象单一性了,所以线程并不安全。
- 懒汉模式线程安全版
public class Singleton3 {
private Singleton3(){}
private static Singleton3 instance = null;
public static synchronized Singleton3 getInstance(){
if(instance == null){
instance = new Singleton3();
}
return instance;
}
}
复制代码
上面的案例,在多线程中工作且线程安全,但是每次调用 getInstance() 方法都需要进行线程锁定判断,在多线程高并发访问环境中,将会导致系统性能下降。事实上,不仅效率很低,99%情况下不需要线程锁定判断。
- 懒汉模式线程安全进阶版
public class Singleton4 {
private Singleton4(){}
private static Singleton4 instance = null;
public static Singleton4 getInstance(){
if(instance == null){
synchronized(Singleton4.class){
if(instance == null){
instance = new Singleton4();
}
}
}
return instance;
}
}
复制代码
这种性能方面就优于前面那种,这种模式等于两次校验,第一次是判断是否创建对象,如果没有再以同步的方式创建对象。
public class Singleton5 {
private Singleton5() {}
private static class SigletonHolder {
private final static Singleton5 instance = new Singleton5();
}
public static Singleton5 getInstance() {
return SigletonHolder.instance;
}
}
复制代码
- 枚举
public enum SingletonEnum {
INSTANCE;
private SingletonEnum(){}
}
复制代码
枚举的特点是,构造方法是 private 修饰的,并且成员对象实例都是预定义的,因此我们通过枚举来实现单例模式非常的便捷。这种模式很少见,而且我们也不常用,本身枚举在性能方面差,占用内存较多,只需要了解即可。
- 静态内部内
public class Singleton5 {
private Singleton5() {}
private static class Sigleton {
private final static Singleton5 instance = new Singleton5();
}
public static Singleton5 getInstance() {
return SigletonHolder.instance;
}
}
复制代码
类加载的时候并不会实例化 Singleton5,而是在第一次调用 getInstance() 加载内部类 Sigleton,此时才进行初始化 instance 成员变量,确保内存中的对象唯一性。
单例模式 vs 静态方法
如果认为单例模式是非静态方法。而静态方法和非静态方法,最大的区别在于是否常驻内存,实际上是不对的。它们都是在第一次加载后就常驻内存,所以方法本身在内存里,没有什么区别,所以也就不存在静态方法常驻内存,非静态方法只有使用的时候才分配内存的结论。
因此,我们要从场景的层面来剖析这个问题。如果一个方法和他所在类的实例对象无关,仅仅提供全局访问的方法,这种情况考虑使用静态类,例如 java.lang.Math。而使用单例模式更加符合面向对象思想,可以通过继承和多态扩展基类。此外,上面的案子中,单例模式还可以进行延伸,对实例的创建有更自由的控制。
volatile 修饰
对象的创建并不是一个原子操作,在 new 对象的时候其实是有 3 步:分配内存,初始化和赋值。由于 java 是允许处理器进行乱序执行的,所以有可能是先赋值再初始化,这样懒汉模式就有异常了,解决方法是给这个静态对象加 volatile 字段来防止乱序执行。
这里对volatile不做详细的解释,感兴趣的可以查看()
总结
我们始终记得一个原则就是:单例模式始终保证一个类只有一个实例对象存在,及唯一性。
如果采用饿汉式,在类被加载时就实例化,因此无须考虑多线程安全问题,并且对象一开始就得以创建,性能方面要优于懒汉式。
如果采用懒汉式,采用延迟加载,在第一次调用 getInstance() 方法时才实例化。好处在于无须一直占用系统资源,在需要的时候再进行加载实例。但是,要特别注意多线程安全问题,我们需要考虑使用双重校验锁的方案进行优化。
实际上,我们应该采用饿汉式还是采用懒汉式,取决于我们希望空间换取时间,还是时间换取空间的抉择问题,因此选择哪种模式,只是取决于我们实际项目中适合哪种。
最后想说一点,静态内部类也是非常不错的实现方式。