首页 > 其他 > 详细

单例模式

时间:2020-07-08 10:16:59      阅读:68      评论:0      收藏:0      [点我收藏+]

单例模式是最简单也是应用最广泛的设计模式之一,其核心思想是某个类在应用程序的生命周期内只有唯一的实例。这可能是考虑到这个类的职责是很耗费资源的,不适合创建多个对象。或者这是个工具类,在程序的其它地方都可能用到,并且只需要一个对象来保证这个工具具有统一的入口。

例如,在Android应用中,我们经常要在应用启动时对一些SDK或模块进行初始化,或者在其它类中为了避免直接使用Activity等Context会造成内存泄漏,都可能要用到ApplicationContext对象。虽然可以通过Context.getApplicationContext()方法来获取,但是在没有Context时就不方便了。因此,我们通常都会写一个Application的子类,并在其中持有Application的单例:

public class MyApplication extends Application {
    
    private static Context applicationContext;
    
    @Override
    public void onCreate() {
        super.onCreate();
        applicationContext = this;
        //TODO init something
    }
    
    public static Context getApplicationContext() {
        return applicationContext;
    }
}

这样,就可以在任何地方调用MyApplication.getApplicationContext()方法来获取这个实例了。

而对于一个普通的类,单例模式是有多种实现方式的:

1.饿汉模式

public class HungryInstance {

    private static HungryInstance instance = new HungryInstance();

    private HungryInstance() {
    }

    public static HungryInstance getInstance() {
        return instance;
    }
}

这种实现方式,静态实例在声明时就会进行初始化,即实例的创建在类第一次访问时的静态初始化阶段就会进行。这里要记得将默认构造方法声明为private,保证只能通过getInstance()方法来获取实例。由于在调用静态方法时,实例已经创建好了,因此这里不用考虑线程同步的问题。

2.懒汉模式

public class LazyInstance {

    private static LazyInstance instance;

    private LazyInstance() {
    }

    public static synchronized LazyInstance getInstance() {
        if (instance == null) {
            instance = new LazyInstance();
        }
        return instance;
    }
}

实例在类加载时不会初始化,只有第一次调用getInstance()方法时才会初始化。这在一定程度上避免了没有使用就初始化而造成的资源浪费,适用于需要懒加载的情况。可以看到,getInstance()方法加了synchronized同步锁,保证了多线程访问时实例的唯一性。但是同步锁在第一次调用后就没有必要了,以后每次调用都会同步会造成不必要的开销,这是这种实现方式的缺点。

3.DCL模式(Double Check Lock)

public class DLCInstance {

private volatile static DLCInstance instance;

private DLCInstance() {
}

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

DLC模式具有懒汉模式懒加载和线程安全的特点,并且同步锁只会在第一次调用时有效,实例创建后就不会再同步了,从而解决了懒汉模式的缺点。这里实例的声明使用了volatile关键字,即具有原子性。如果不加volatile关键字,instance = new DLCInstance()就是一个非原子的操作,就会存在DLC失效问题。因为在JDK1.5之前,Java内存模型中Cache、寄存器到主存的回写顺序规定以及Java编译器允许处理器乱序执行等原因,可能会出现构造方法执行前,instance指针已经指向了实例所占用的内存空间,即instance不为null。此时如果另一个线程执行到了外层的判空,就会直接返回这个空的instance,从而造成空指针。增加volatile关键字后,因为原子操作是不能被线程调度机制打断的,volatile域会立即被写入到主存中,读取也会发生在主存中,就可以保证instance对象每次都会从主存中读取,从而避免了另一个线程拿到空的instance的情况。(参考《Java编程思想》《Android源码设计模式》)

4.静态内部类模式

public class StaticInnerInstance {

    private StaticInnerInstance() {
    }

    public static StaticInnerInstance getInstance() {
        return InstanceHolder.instance;
    }

    private static class InstanceHolder {
        private static final StaticInnerInstance instance = new StaticInnerInstance();
    }
}

这种实现方式同样具有DLC模式的优点。因为instance是在静态内部类中声明和初始化的,是线程安全的。而在调用getInstance()方法前,实例也不会初始化,从而又是懒加载的。并且代码很简洁,不需要判空,是推荐的实现方式。

但是,以上的方式都存在一个问题,都没有处理对象序列化再反序列化后对象的唯一性。因为Serializable序列化对象不会调用构造方法,而是以它存储的二进制位为基础来构造的。当我们反序列化一个单例时,序列化机制会创建一个新的实例。为了解决这个问题,必须在类中加入readResolve()方法:

public class HungryInstance {

    private static HungryInstance instance = new HungryInstance();

    private HungryInstance() {
    }

    public static HungryInstance getInstance() {
        return instance;
    }
    
    private Object readResolve() throws ObjectStreamException {
        return instance;
    }
}

当对象被序列化后,就会调用这个方法,它返回的对象将成为反序列化时readObject的返回值。因此,这里我们直接返回我们定义的单例,这样就可以解决反序列化单例不唯一的问题。

5.枚举模式

public enum EnumInstance {

    INSTANCE;

    public void doSomething() {
        System.out.println("do something");
    }
}

使用枚举实现将更加简洁。Java中的enum基本上可以看作是一个常规的类,可以定义字段和方法,只是构造方法有少许限制。enum的构造方法只能在内部用来创建实例,enum定义结束后,Java编译器就不允许使用它来创建任何实例了。因此上面我们声明的这个INSTANCE实例在任何情况下都是一个单例,并且实例的创建也是线程安全的。而enum在反序列化时也不会重新创建对象,也就不用添加readResolve()方法。

在Android开发中,谷歌早已开始宣传Kotlin First的口号。用Kotlin实现单例模式将更加简洁:

object SingleInstance {
    
    fun doSomething() {
        //TODO something
    }
}

Kotlin自带object关键字表示单例类,Kotlin也弱化了静态的概念,我们直接在单例类中定义方法或字段,即可像调用静态方法一样直接调用单例方法SingleInstance.doSomething()。之所以这么简洁,是因为Kotlin将单例的逻辑隐藏了,我们可以通过Android Studio的Show Kotlin Bytecode功能将上述Kotlin代码转换为等价的Java代码:

public final class SingleInstance {
   public static final SingleInstance INSTANCE;

   public final void doSomething() {
   }

   private SingleInstance() {
   }

   static {
      SingleInstance var0 = new SingleInstance();
      INSTANCE = var0;
   }
}

可以看到,实例的初始化放在了静态块里。静态块只会在类首次访问静态成员或创建对象时执行一次,因此Kotlin的单例类可以看作是Java单例的饿汉模式实现。

虽然单例模式的实现方式有多种,但我们只要理解了其核心思想,并根据类的职责,例如是否需要懒加载,是否需要线程安全,是否会进行序列化和反序列化等来选择合适的方式实现即可。

单例模式

原文:https://www.cnblogs.com/wanghan5950/p/13252628.html

(0)
(0)
   
举报
评论 一句话评论(0
关于我们 - 联系我们 - 留言反馈 - 联系我们:wmxa8@hotmail.com
© 2014 bubuko.com 版权所有
打开技术之扣,分享程序人生!