前言

继上一篇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() 方法时才实例化。好处在于无须一直占用系统资源,在需要的时候再进行加载实例。但是,要特别注意多线程安全问题,我们需要考虑使用双重校验锁的方案进行优化。

实际上,我们应该采用饿汉式还是采用懒汉式,取决于我们希望空间换取时间,还是时间换取空间的抉择问题,因此选择哪种模式,只是取决于我们实际项目中适合哪种。

最后想说一点,静态内部类也是非常不错的实现方式。