一、概述
在程序设计中,一个类保证只有一个实例,并且提供访问这个类的统一入口,那么这种模式就是单例模式。使用单例模式的的优点:
- 节省内存资源。在内存中只有一个对象实例,减少了内存开销,特别是像需要频繁创建对象的网络请求、数据库/文件访问、图片等,单例模式的优势很明显。
- 方便管理。对外部的访问提供了统一的入口,避免对资源的多重占用,优化和共享资源的访问。
二、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那样复杂了。