编程

当前位置:时时彩平台 > 编程 > 单例模式

单例模式

来源:http://www.mrmtshipyard.com 作者:时时彩平台 时间:2019-10-06 08:00

单例模式写法大全,也许有你不知道的写法

单例模式(Singleton)相信大家或多或少都用过,代码量不多,看起来也很简单,但是里面的学问却不简单。面试中经常会问到单例模式相关的问题,但是如果你只停留在最简单的使用上,那么想让面试官满意就很难了。单例模式用来保证应用中有且仅有一个实例,也是Android中用的较多的一种设计模式。单例模式有很多使用场景,例如,当一个对象需要频繁创建、销毁时,为了减少内存开销,可以使用单例模式。单例模式最关键的两点:**1、私有构造方法2、公有的对外调用接口**

  • 引言
  • 什么是单例?
  • 单例模式作用
  • 单例模式的实现方法

单例模式分为两种,饿汉式和懒汉式。常见简单写法如下所示:

单例模式想必是大家接触的比较多的一种模式了,就算没用过但是肯定听过他的鼎鼎大名了。在我初入编程界时听到最多的就是单例模式,工厂模式,观察者模式了。特别是观察者模式在Android开发中几乎是随处可见,不过今天我们先来学习一个看似简单很多的单例模式。

一、饿汉式

 public class Singleton{ private static final Singleton instance = new Singleton(); //声明为static和final,第一次加载类到内存中时就会初始化 private Singleton(){} public static Singleton newInstance(){ return instance; }}

单例模式确保某一个类只有一个实例。

二、懒汉式

 public class Singleton{ private static Singleton instance = null; private Singleton(){} public static Singleton newInstance(){ if(instance == null){ instance = new Singleton(); //方法被调用的时候才生成实例 } return instance; }}

为什么要确保一个类只有一个实例?有什么时候才需要用到单例模式呢?听起来一个类只有一个实例好像没什么用呢!那我们来举个例子。比如我们的APP中有一个类用来保存运行时全局的一些状态信息,如果这个类实现不是单例的,那么App里面的组件能够随意的生成多个类用来保存自己的状态,等于大家各玩各的,那这个全局的状态信息就成了笑话了。而如果把这个类实现成单例的,那么不管App的哪个组件获取到的都是同一个对象(比如Application类,除了多进程的情况下)。

三、饿汉式与懒汉式的区别

对比这两种方式的代码我们可以看出,饿汉式在类第一次加载进内存就实例化了,而懒汉式使用了懒加载模式,当方法被调用时类对象的实例时才生成。根据名称也很好理解,饿汉式很饿,刚一加载进内存,不等你说,就迫不及待生成“食物”充饥;懒汉式很懒,需要的时候你催我,我才给你生成。

单例模式的定义和功能都是比较简单清楚的东西,那么到底怎么实现这个模式呢?

四、饿汉式与懒汉式的适用场景

饿汉式是最简单的实现方式,适合那些在初始化时就要用到单例的情况,如果单例对象初始化非常快,而且占用内存非常小的时候这种方式是比较合适的,可以直接在应用启动时加载并初始化。饿汉式的创建方式在一些场景中无法使用:譬如 Singleton 实例的创建是依赖参数或者配置文件的,在 getInstance() 之前必须调用某个方法设置参数给它,那么这种单例写法就无法使用了。 懒汉式将单例的初始化操作延迟到需要的时候才进行,若某个单例用的次数不是很多,但是这个单例提供的功能又非常复杂,而且加载和初始化要消耗大量的资源,这个时候使用懒汉式就比使用饿汉式合适的多。

1.饿汉式

可能有的小伙伴们会想到利用Java的静态域初始化机制来实现

public class SimpleSingleton { private static SimpleSingleton instance=new SimpleSingleton(); /** * 构造方法私有化,几乎所有的单例模式实现都会将构造方法私有化 */ private SimpleSingleton() { } public static SimpleSingleton getInstance(){ return instance; }}

这种写法很简单,先把构造函数设为private的(几乎大部分的单例写法都会这么做)。然后在类中设置一个静态的字段,并调用构造函数。这样jvm在加载这个类的时候就会自动初始化这个类,接着每次需要使用的时候都调用getInstance方法获得类实例。而java的语法规则保证new SimpleSingleton()自会调用一次。

使用这种方式实现的单例是线程安全的(这一点是由jvm保证的),并且在类加载的时候就已经生成了一个实例,当调用的时候get获取这个实例是就非常快。

凡事都有优缺点,饿汉式单例也不例外。他的一个很明显的缺点就是在性能上。jvm会在加载类的时候直接初始化实例,而如果这个类的实例在应用中使用频率并不高,有的时候整个App从被用户打开到结束都不会使用一次这个类实例,那么这个初始化的操作就是完全浪费了。

为了解决这个问题,我们想了一种新的实现方式。

五、线程安全的懒汉式

不知道大家发现没有,在上面的代码中关于懒汉式的写法存在一个致命的缺点,那就是在多线程的情况下无法正常使用,当多个线程同时调用getInstance()方法时,就会创建多个Singleton实例,因此这种写法是线程不安全的。而饿汉式只会在Singleton类加载进内存时实例化一次,不会出现多次实例化的问题,也就不存在线程安全问题。

为了解决懒汉式的线程安全问题,最简单的方法是使用同步锁synchronized

public class Singleton { private static Singleton instance = null; private Singleton(){ } public static Singleton getInstance() { synchronized (Singleton.class) {//防止多线程同时进入造成instance被多次实例化 if (instance == null) { instance = new Singleton(); } } return instance; }}

这样做虽然解决了线程安全问题,但是并不高效,因为在任何时候只能有一个线程调用 getInstance() 方法。我们的同步并不是要防止多个线程同时调用getInstance(),而是防止多个线程同时实例化instatnce。因此,可以在实例化instatnce的地方进行同步,如下所示:

public class Singleton { private static Singleton instance = null; private Singleton(){ } public static Singleton getInstance() { if (instance == null) { //-------------------Single Checked // 若实例创建了,则不需要同步了,直接返回instance即可 synchronized (Singleton.class) { //未创建实例,加锁 if (instance == null) { //----------------------Double Checked //若被同步的线程中有一个线程创建了实例,那么别的线程就不用创建了 instance = new Singleton(); } } } return instance; }}

其中第二次Check是因为可能会有多个线程一起进入同步块外的 if,如果在同步块内不进行二次检验的话就会生成多个实例了。

上面的Double-check看似完美了,但是却存在问题,因为 instance = new Singleton();这句代码并不是原子操作。在JVM中这句代码大概做了下面 3 件事情:给 instance 分配内存调用 Singleton 的构造函数来初始化成员变量,形成实例将instance对象指向分配的内存空间(执行完这步 instance 才为非 null)但是在 JVM 的即时编译器中存在指令重排序的优化。也就是说上面的第步的顺序是不能保证的,最终的执行顺序可能是 1-2-3 也可能是 1-3-2。如果是后者,则在 3 执行完毕、2 未执行之前,被其他线程抢占了,这时 instance 已经是非 null 了,所以其他线程会直接返回 instance,然后使用这个未初始化的instance,就会顺理成章地报错。解决办法也很简单,只需要将 instance 变量声明成 volatile 就可以了。我们一般使用volatile关键字有两个功能:可见性:volatile关键字修饰的变量不会在多个线程中存在副本,每次都是直接从内存中读取。禁止指令重排序优化。在 volatile 变量的赋值操作后面会有一个内存屏障,读操作不会被重排序到内存屏障之前。比如上面的例子,取操作必须在执行完 1-2-3 之后或者 1-3-2 之后,不存在执行到 1-3 然后取到值的情况。

public class Singleton { private volatile static Singleton instance = null; //声明成 volatile private Singleton(){ } public static Singleton getInstance() { if (instance == null) { //-------------------Single Checked // 若实例创建了,则不需要同步了,直接返回instance即可 synchronized (Singleton.class) { //未创建实例,加锁 if (instance == null) { //----------------------Double Checked //若被同步的线程中有一个线程创建了实例,那么别的线程就不用创建了 instance = new Singleton(); } } } return instance; }}

2.懒汉式

public class ServiceNotThreadSafe { public static ServiceNotThreadSafe INSTANCE = null; /** * 必备操作 */ private ServiceNotThreadSafe() { } /** * @return instance */ public static ServiceNotThreadSafe getInstance() { if (INSTANCE == null) { INSTANCE = new ServiceNotThreadSafe(); } return INSTANCE; }}

懒汉式的写法有一个懒加载的效果,只有当第一次调用getInstance方法时才会去实例化一个对象。可能细心的小伙伴已经注意到这个很直白的类名了NotThreadSafe。没错这种写法在单线程中没有任何问题,但是在并发程序中就无法保证 类实例只有一个的情况了。

为了解决上面的问题,我们相出了一个新的写法

六、静态内部类

public class Singleton { private static class SingletonHolder {//只有加载内部类的时候才初始化 private static final Singleton INSTANCE = new Singleton(); } private Singleton (){} public static final Singleton getInstance() { //只有在getInstance()被调用的时候才被真正创建 return SingletonHolder.INSTANCE; }}

这种写法仍然使用JVM本身机制保证了线程安全问题。 由于 SingletonHolder 是私有的,除了 getInstance() 之外没有办法访问它,因此它是懒汉式的;同时读取实例的时候不会进行同步,没有性能缺陷;也不依赖 JDK 版本。这种写法即解决了饿汉式不能延迟加载的缺陷,又解决了懒汉式线程安全的问题,也是《Effective Java》上所推荐的写法。

3.懒汉式 —— 单锁定法

public class ServiceThreadSafe { public static ServiceThreadSafe instance; /** * 还是常规操作的私有构造函数 */ private ServiceThreadSafe() { } public static synchronized ServiceThreadSafe getInstance(){ if (instance==null){ instance=new ServiceThreadSafe(); } return instance; }}

这种写法没什么好说的,只是在getInstance方法上加了一个内置同步锁,从而保证了线程安全。但是也因此引入了一个新的问题 —— 同步锁范围太大,影响并发性能(在getInstance方法并不是频繁调用下问题不大)。

为了解决这个问题,聪明的程序员们想到了另一种写法。

七、枚举

public enum Singleton{ INSTANCE; //定义一个枚举的元素,它就是Singleton的一个实例 }

我们可以通过Singleton.INSTANCE来访问实例,这比调用getInstance()方法简单多了。默认枚举实例的创建是线程安全的(创建枚举类的单例在JVM层面也是能保证线程安全的),所以不需要担心线程安全的问题,而且还能防止反序列化导致重新创建新的对象。所以理论上枚举类来实现单例模式是最简单的方式。但由于大多数人不太熟悉,用这种方法写的人不多。

4.双重锁定法

public class ServiceDoubleCheck { /** * 注意这里加了 volatile 修饰符,用来保证内存可见性。 * 具体各位小伙伴可以Google一下。 * 如果你没加这个修饰符的话,那么具体结果只能看编译器,jvm和cpu的心情了OO~~ */ public static volatile ServiceDoubleCheck instance = null; /** * 大家都懂得操作 */ private ServiceDoubleCheck() { } /** * 理论上只要第一次的时候才会完全走完整个方法,之后进入这个方法时instance==null都不成立 * 而不用在进入内部的同步代码块,带来新能上的优势 * @return */ public static ServiceDoubleCheck getInstance() { if (instance == null) { synchronized (ServiceDoubleCheck.class) { if (instance == null) { instance = new ServiceDoubleCheck(); } } } return instance; }}

优点:

  • 资源利用率高,懒加载的形式,不使用就不会实例化
  • 线程安全缺点:
  • 写法略微繁琐
  • 第一次加载时速度不快

八、总结

一般来说,单例模式有五种写法:饿汉、懒汉、双重检验锁、静态内部类、枚举。上述所说都是线程安全的实现,文章开头给出的第一种懒汉式方法不算正确的写法。 在我们日常实践中,一般情况下直接使用饿汉式就好了,如果明确要求要懒加载,则推荐使用静态内部类,如果涉及到反序列化创建对象时,可以使用枚举的方式来实现单例。但是在很多的面试中,线程安全的懒汉式是最常考察的,我们需要好好掌握Double-Check的写法,注意关键字volatile的使用。参考文章:1、

5.懒汉式静态内部类写法

public class ServiceInner { /** * 实现一个静态内部类 */ private static class Instance{ private static ServiceInner instance=new ServiceInner(); } public static ServiceInner getInstance(){ return Instance.instance; } private ServiceInner() { }}

优点:

  • 线程安全
  • 懒加载形式,资源利用率高缺点:
  • 第一次加载速度不快

6.枚举实现 —— 一个《effective java》 作者都推荐的方法

public class Resources { public enum ResourcesInstance { INSTANCE; private Resources instance; ResourcesInstance() { this.instance = new Resources(); } public Resources getInstance() { return instance; } }}

之前介绍过的哪些单例实现都有一个问题,就是不能保证序列化生成另一个实例。比如先序列化写入到文件,然后再从文件读取反序列化回来,这样子我们就会得到两个实例,这就违背了单例的原则。而用枚举实现能解决这个问题。

基本上单例的实现方法都介绍完了,一般实际应用中如果对象的实例化并不是很耗费资源的话使用最简单的饿汉法就行了。如果需要对象懒加载则可以选用双重锁定法(如果不需要考虑线程安全的话可以使用简单的懒汉式)。而要在序列化的过程中保证单例的话就要使用枚举的方法来实现了。

本文由时时彩平台发布于编程,转载请注明出处:单例模式

关键词:

上一篇:spark RDD,reduceByKey vs groupByKey

下一篇:没有了