设计模式——单例模式

1,121 阅读5分钟

一、概述

在程序设计中,一个类保证只有一个实例,并且提供访问这个类的统一入口,那么这种模式就是单例模式。使用单例模式的的优点:

  • 节省内存资源。在内存中只有一个对象实例,减少了内存开销,特别是像需要频繁创建对象的网络请求、数据库/文件访问、图片等,单例模式的优势很明显。
  • 方便管理。对外部的访问提供了统一的入口,避免对资源的多重占用,优化和共享资源的访问。

二、Java单例模式

Java的单例模式通常有5种实现方式:饿汉式、懒汉式、双重验证锁、静态内部类、枚举。下面分别对这几种写法进行具体的说明。

1、饿汉式

public static class SingletonModeOne {
    private static final SingletonModeOne singletonModeOne = new SingletonModeOne();

    private SingletonModeOne() {
    }

    public static SingletonModeOne getInstance() {
        return singletonModeOne;
    }

    public void showMsg(String msg) {
        Log.e(TAG, "SingletonModeOne ---> showMsg:" + msg);
    }
}
  • 构造方法使用private修饰,外界无法直接创建实例。
  • 申明静态对象的时候就直接初始化。
  • 使用static修饰为静态变量,存储在内存中只有1份数据;使用final修改,只初始化一次,所以singletonModeOne实例只有1个。

优点:获取对象的速度快;线程安全,无需同步块。

缺点:类加载较慢;不能延迟加载,如果单例没有使用的话,就造成内存资源的浪费;无法防止反射和反序列化调用。

综上所述,不推荐使用饿汉式单例模式。

2、懒汉式

public static class SingletonModeTwo {

    private static SingletonModeTwo singletonModeTwo;

    private SingletonModeTwo() {
    }

    public static synchronized SingletonModeTwo getInstance() {
        if (singletonModeTwo == null) {
            singletonModeTwo = new SingletonModeTwo();
        }
        return singletonModeTwo;
    }

    public void showMsg(String msg) {
        Log.e(TAG, "SingletonModeOne ---> showMsg:" + msg);
    }
}
  • 构造方法使用private修饰,外界无法直接创建实例。
  • 使用static修饰为静态变量,存储在内存中只有1份数据。

优点:只有调用了getInstance()方法才会初始化对象,达到了延迟加载的目的;线程安全。

确定:使用效率不高,每次调用getInstance()方法都会进行同步,造成内存资源的浪费;无法防止反射和反序列化调用。

综上所述,不推荐使用懒汉式单例模式。

3、双重验证锁

public static class SingletonModeThree {

    private static volatile SingletonModeThree singletonModeThree = null;

    private SingletonModeThree() {
    }

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

    public void showMsg(String msg) {
        Log.e(TAG, "SingletonModeFour ---> showMsg:" + msg);
    }
}
  • 构造方法使用private修饰,外界无法直接创建实例。
  • 使用static修饰为静态变量,存储在内存中只有1份数据。

优点:只有调用了getInstance()方法才会初始化对象,达到了延迟加载的目的;线程安全;使用volatile修饰实例,防止指令重排序的问题,保证对象在线程中的可见性;双重判断,避免了无用的同步,减少了内存的开销。

缺点:无法防止反射和反序列化调用;在Java 5之前版本volatile 的双检锁还是有问题的。其原因是Java 5以前的JMM(Java 内存模型)是存在缺陷的,即时将变量声明成volatile也不能完全避免重排序,主要是volatile 变量前后的代码仍然存在重排序问题。这个volatile屏蔽重排序的问题在Java 5中才得以修复。

综上所述,在java 5之前不推荐使用。

4、静态内部类

 public static class SingletonModeFour {

    private SingletonModeFour() {
    }

    private static class InnerSingletonMode {
        private static SingletonModeFour singletonModeFour = new SingletonModeFour();
    }

    public static SingletonModeFour getInstance() {
        return InnerSingletonMode.singletonModeFour;
    }

    public void showMsg(String msg) {
        Log.e(TAG, "SingletonModeThree ---> showMsg:" + msg);
    }
}
  • 构造方法使用private修饰,外界无法直接创建实例。

优点:只有调用了getInstance()方法才会初始化对象,达到了延迟加载的目的;调用效率高;线程安全。

缺点:无法防止反射和反序列化调用。

综上所述:推荐使用。

5、枚举

public enum SingletonModeFive {
    singletonModeFive;

    public void showMsg(String msg) {
        Log.e(TAG, "SingletonModeFive ---> showMsg:" + msg);
    }
}

优点:写法简单;线程安全;调用效率高;可以防止反射和反序列化调用。

缺点:不能延迟加载。

综上所述:推荐使用。

三、kotlin单例模式

1、普通单例

kotlin的单例模式和Java有很大的区别,因为kotlin没有static这个关键字,也没有静态这么一说。但是kotlin可以使用object关键字以及companion object伴生对象可以实现类似于Java中的静态方法和静态变量。关于kotlin中的object以及companion object的使用这里就不做说明,具体参照其他的kotlin文档。

看到有很多文章把上面Java的5中常用单例模式,分别写出了kotlin版本的,其实这些写法完全就是Java的思想,而不是kotlin的思想,根据kotlin的官方文档说明,如果需要一个单例,则可以按照通常的方式声明该类,使用object关键字而不是class,这里可以查看官方文档

If you need a singleton - a class that only has got one instance - you can declare the class in the usual way, but use the object keyword instead of class

object SingletonModeOne {
    fun showMsg(str: String) {
        println("SingletonModeOne ---> showMsg:${str}")
    }
}

There will only ever be one instance of this class, and the instance (which is created the first time it is accessed, in a thread-safe manner) has got the same name as the class

该类将永远只有一个实例,并且该实例(在首次访问它时创建,并以线程安全的方式)具有与该类相同的名称。那么就说明了以这种方式创建的单例是:

  • 内存中只会存在一个实例。
  • 外界无法直接通过构造方法创建实例(这是object的限制)。
  • 首次访问的时候才会创建实例,说明是延迟加载的。
  • 线程安全。

把上面的kotlin代码转化成java代码后,我们看看是怎么回事:

public final class SingletonModeOne {
   public static final SingletonModeOne INSTANCE;

   private SingletonModeOne() {
   }

   static {
        SingletonModeOne var0 = new SingletonModeOne();
        INSTANCE = var0;
   }
   
   public final void showMsg(@NotNull String str) {
        Intrinsics.checkParameterIsNotNull(str, "str");
        String var2 = "SingletonModeOne ---> showMsg:" + str;
        boolean var3 = false;
        System.out.println(var2);
   }
}

先把showMsg这个方法忽略。可以看到有一个static的静态代码块,当类首次加载时,会执行这个静态代码块,所以达到了延迟加载以及线程安全的效果,可以看到构造方法用了private修饰,所以外界无法直接构造对象,也无法防止反射和反序列化调用。其实有点类似于java的饿汉式,也没有什么特殊的地方。

2、带参数的单例

我们在项目中经常会遇到需要传递一个Context参数才能构造一个对象。之前的单例模式我们都是不带构造参数的,也不推荐带参数,因为单例模式可能会长时间的在内存驻留,特别是访问携带很多资源的方法时,使得内存占用一直居高不下,并且持有Context引用,很有可能会造成内存泄漏,甚至导致OOM的可能。但有时候我们设计的程序不得不这样做,有2种方式可以避免:1.在单例中采用注入Application的方式,引用全局的Context,而不是其他的Context;2.是封装一个双重检查锁的方式。

方式一

object SingletonModeFour {
        private var context: Context? = null
        fun init(context: Context?) {
            this.context = context
        }

        fun showMsg(str: String) {
            println("SingletonModeTwo ---> showMsg:${str}")
        }
    }
class MyApp : Application() {

    override fun onCreate() {
        super.onCreate()
        SingletonModeForKotlin.SingletonModeFour.init(this)
    }
}

方式二

class SingletonModeTwo private constructor(myContext: Context) {
    private val mContext: Context = myContext

    companion object {

        @Volatile
        private var instance: SingletonModeTwo? = null

        fun getInstance(context: Context): SingletonModeTwo {
            val i = instance
            if (i != null) {
                return i
            }
            return synchronized(this) {
                val i2 = instance
                if (i2 != null) {
                    i2
                } else {
                    val i3 = SingletonModeTwo(context)
                    instance = i3
                    i3
                }
            }
        }
    }

    fun showMsg(str: String) {
        println("SingletonModeTwo ---> showMsg:${str}")
    }
}

3、枚举单例

kotlin的枚举单例和java类似,具体的就不用再多说了。

enum class SingletonModeThree{
    Instance;
    fun showMsg(str: String) {
        println("SingletonModeThree ---> showMsg:${str}")
    }
}

四、总结

单例模式在代码中使用的频率应该是最多的,比如网络请求,图片加载,IO操作等需要消耗很多资源的类,设计成单例会有很多好处,需要注意的是保证在多线程下的单例。在java中推荐使用静态内部类和枚举,kotlin的单例和Java有很大的不同,也有语言的因素在其中,kotlin的单例其实有点像语法糖,为我们减少了很多代码,而且在kotlin中使用单例,就不需要像java那样复杂了。

项目地址:github.com/leewell5717…

五、参考

Android 单例模式

Kotlin 设计模式解析系列之单例模式

聊聊Kotlin单例,从object单例,到带参数单例,论如何优雅的封装!