《Android 源码设计模式解析与实战》——第2章,第2.6节单例模式的其他实现方式

2.6 单例模式的其他实现方式

2.6.1 懒汉模式
懒汉模式是声明一个静态对象,并且在用户第一次调用getInstance时进行初始化,而上述的饿汉模式(CEO类)是在声明静态对象时就已经初始化。懒汉单例模式实现如下。

public class Singleton {
  private static Singleton instance;  
  private Singleton () {}

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

读者可能已经发现了,getInstance()方法中添加了synchronized关键字,也就是getInstance是一个同步方法,这就是上面所说的在多线程情况下保证单例对象唯一性的手段。细想一下,大家可能会发现一个问题,即使instance已经被初始化(第一次调用时就会被初始化instance),每次调用getInstance方法都会进行同步,这样会消耗不必要的资源,这也是懒汉单例模式存在的最大问题。

最后总结一下,懒汉单例模式的优点是单例只有在使用时才会被实例化,在一定程度上节约了资源;缺点是第一次加载时需要及时进行实例化,反应稍慢,最大的问题是每次调用getInstance都进行同步,造成不必要的同步开销。这种模式一般不建议使用。

2.6.2 Double CheckLock ( DCL )实现单例
DCL方式实现单例模式的优点是既能够在需要时才初始化单例,又能够保证线程安全,且单例对象初始化后调用getInstance不进行同步锁。代码如下所示:

public class Singleton {
  private static Singleton sInstance = null;
  private Singleton() {
  }
  public void doSomething() {
    System.out.println("do sth.");
  }

  public static Singleton getInstance() {
    if (mInstance == null) {
      synchronized (Singleton.class) {
        if (mInstance == null) {
          sInstance = new Singleton();
        }
      }
    }
    return sInstance;
  }

本程序的亮点自然都在getInstance方法上,可以看到getInstance方法中对instance进行了两次判空:第一层判断主要是为了避免不必要的同步,第二层的判断则是为了在null的情况下创建实例。这是什么意思呢?是不是有点摸不着头脑,下面就一起来分析一下。

假设线程A执行到sInstance = new Singleton()语句,这里看起来是一句代码,但实际上它并不是一个原子操作,这句代码最终会被编译成多条汇编指令,它大致做了3件事情:

(1)给Singleton的实例分配内存;

(2)调用Singleton()的构造函数,初始化成员字段;

(3)将sInstance对象指向分配的内存空间(此时sInstance就不是null了)。

但是,由于Java编译器允许处理器乱序执行,以及JDK1.5之前JMM(Java Memory Model,即Java内存模型)中Cache、寄存器到主内存回写顺序的规定,上面的第二和第三的顺序是无法保证的。也就是说,执行顺序可能是也可能是1-3-2。如果是后者,并且在3执行完毕、2未执行之前,被切换到线程B上,这时候sInstance因为已经在线程A内执行过了第三点,sInstance已经是非空了,所以,线程B直接取走sInstance,再使用时就会出错,这就是DCL失效问题,而且这种难以跟踪难以重现的错误很可能会隐藏很久。

在JDK1.5之后,SUN官方已经注意到这种问题,调整了JMM、具体化了volatile关键字,因此,如果JDK是1.5或之后的版本,只需要将sInstance的定义改成private volatile static Singleton sInstance = null就可以保证sInstance对象每次都是从主内存中读取,就可以使用DCL的写法来完成单例模式。当然,volatile或多或少也会影响到性能,但考虑到程序的正确性,牺牲这点性能还是值得的。

DCL的优点:资源利用率高,第一次执行getInstance时单例对象才会被实例化,效率高。缺点:第一次加载时反应稍慢,也由于Java内存模型的原因偶尔会失败。在高并发环境下也有一定的缺陷,虽然发生概率很小。DCL模式是使用最多的单例实现方式,它能够在需要时才实例化单例对象,并且能够在绝大多数场景下保证单例对象的唯一性,除非你的代码在并发场景比较复杂或者低于JDK 6版本下使用,否则,这种方式一般能够满足需求。

2.6.3 静态内部类单例模式
DCL虽然在一定程度上解决了资源消耗、多余的同步、线程安全等问题,但是,它还是在某些情况下出现失效的问题。这个问题被称为双重检查锁定(DCL)失效,在《Java并发编程实践》一书的最后谈到了这个问题,并指出这种“优化”是丑陋的,不赞成使用。而建议使用如下的代码替代:

public class Singleton {
  private Singleton() { }
  public static Singleton getInstance () {
    return SingletonHolder.sInstance;
  }

  /**
   * 静态内部类
   */
  private static class SingletonHolder {
    private static final Singleton sInstance = new Singleton();
  }
}

当第一次加载Singleton类时并不会初始化sInstance,只有在第一次调用Singleton的getInstance方法时才会导致sInstance被初始化。因此,第一次调用getInstance方法会导致虚拟机加载SingletonHolder类,这种方式不仅能够确保线程安全,也能够保证单例对象的唯一性,同时也延迟了单例的实例化,所以这是推荐使用的单例模式实现方式。

2.6.4 枚举单例
前面讲解了一些单例模式实现方式,但是,这些实现方式不是稍显麻烦就是会在某些情况下出现问题。还有没有更简单的实现方式呢?我们看看下面的实现:

public enum SingletonEnum {
  INSTANCE;
  public void doSomething() {
    System.out.println("do sth.");
  }
}

什么?枚举!没错,就是枚举!

写法简单是枚举单例最大的优点,枚举在Java中与普通的类是一样的,不仅能够有字段,还能够有自己的方法。最重要的是默认枚举实例的创建是线程安全的,并且在任何情况下它都是一个单例。

为什么这么说呢?在上述的几种单例模式实现中,在一个情况下它们会出现重新创建对象的情况,那就是反序列化。

通过序列化可以将一个单例的实例对象写到磁盘,然后再读回来,从而有效地获得一个实例。即使构造函数是私有的,反序列化时依然可以通过特殊的途径去创建类的一个新的实例,相当于调用该类的构造函数。反序列化操作提供了一个很特别的钩子函数,类中具有一个私有的、被实例化的方法readResolve(),这个方法可以让开发人员控制对象的反序列化。例如,上述几个示例中如果要杜绝单例对象在被反序列化时重新生成对象,那么必须加入如下方法:

private Object readResolve() throws ObjectStreamException {
  return sInstance;
}

也就是在readResolve方法中将sInstance对象返回,而不是默认的重新生成一个新的对象。而对于枚举,并不存在这个问题,因为即使反序列化它也不会重新生成新的实例。

2.6.5 使用容器实现单例模式
在学习了上述各类单例模式的实现之后,再来看看一种另类的实现,具体代码如下:

  public class SingletonManager {
  private static Map<String, Object> objMap = new HashMap<String,Object>();

  private Singleton() { }
  public static void registerService(String key, Objectinstance) {
    if (!objMap.containsKey(key) ) {
      objMap.put(key, instance) ;
    }
  }

  public static ObjectgetService(String key) {
    return objMap.get(key) ;
  }
}

在程序的初始,将多种单例类型注入到一个统一的管理类中,在使用时根据key获取对象对应类型的对象。这种方式使得我们可以管理多种类型的单例,并且在使用时可以通过统一的接口进行获取操作,降低了用户的使用成本,也对用户隐藏了具体实现,降低了耦合度。

不管以哪种形式实现单例模式,它们的核心原理都是将构造函数私有化,并且通过静态方法获取一个唯一的实例,在这个获取的过程中必须保证线程安全、防止反序列化导致重新生成实例对象等问题。选择哪种实现方式取决于项目本身,如是否是复杂的并发环境、JDK版本是否过低、单例对象的资源消耗等。

时间: 2024-10-29 13:15:42

《Android 源码设计模式解析与实战》——第2章,第2.6节单例模式的其他实现方式的相关文章

《Android 源码设计模式解析与实战》——第1章,第1.1节优化代码的第一步——单一职责原则

第1章 走向灵活软件之路--面向对象的六大原则Android 源码设计模式解析与实战 1.1 优化代码的第一步--单一职责原则单一职责原则的英文名称是Single Responsibility Principle,缩写是SRP.SRP的定义是:就一个类而言,应该仅有一个引起它变化的原因.简单来说,一个类中应该是一组相关性很高的函数.数据的封装.就像老师在<设计模式之禅>中说的:"这是一个备受争议却又及其重要的原则.只要你想和别人争执.怄气或者是吵架,这个原则是屡试不爽的".

《Android 源码设计模式解析与实战》——第2章,第2.1节单例模式介绍

第2章 应用最广的模式--单例模式Android 源码设计模式解析与实战 2.1 单例模式介绍单例模式是应用最广的模式之一,也可能是很多初级工程师唯一会使用的设计模式.在应用这个模式时,单例对象的类必须保证只有一个实例存在.许多时候整个系统只需要拥有一个全局对象,这样有利于我们协调系统整体的行为.如在一个应用中,应该只有一个ImageLoader实例,这个ImageLoader中又含有线程池.缓存系统.网络请求等,很消耗资源,因此,没有理由让它构造多个实例.这种不能自由构造对象的情况,就是单例模

《Android 源码设计模式解析与实战》——导读

目 录 自序一 自序二 前言 致谢 第1章 走向灵活软件之路--面向对象的六大原则 1.1节优化代码的第一步--单一职责原则1.2节让程序更稳定.更灵活--开闭原则1.3节构建扩展性更好的系统--里氏替换原则1.4节让项目拥有变化的能力--依赖倒置原则1.5节系统有更高的灵活性--接口隔离原则1.6节更好的可扩展性--迪米特原则1.7节总结 第2章 应用最广的模式--单例模式 2.1节单例模式介绍2.2节单例模式的定义2.3节单例模式的使用场景2.4节单例模式UML类图2.5节单例模式的简单示例

《Android 源码设计模式解析与实战》——第2章,第2.7节Android源码中的单例模式

2.7 Android源码中的单例模式在Android系统中,我们经常会通过Context获取系统级别的服务,如WindowsManagerService.ActivityManagerService等,更常用的是一个LayoutInflater的类,这些服务会在合适的时候以单例的形式注册在系统中,在我们需要的时候就通过Context的getSystemService(String name)获取.我们以LayoutInflater为例来说明,平时我们使用LayoutInflater较为常见的地

《Android 源码设计模式解析与实战》——第2章,第2.8节无名英雄——深入理解LayoutInflater

2.8 无名英雄--深入理解LayoutInflater LayoutInflater在我们的开发中扮演着重要的角色,但很多时候我们都不知道它的重要性,因为它的重要性被隐藏在了Activity.Fragment等组件的光环之下. LayoutInflater是一个抽象类,具体代码如下: public abstract class LayoutInflater { // 代码省略 } 既然是抽象不是具体的,那我们必须把这个深藏功与名的"家伙"找出来!需要先从layoutInflater的

《Android 源码设计模式解析与实战》——第2章,第2.5节单例模式的简单示例

2.5 单例模式的简单示例单例模式是设计模式中比较简单的,只有一个单例类,没有其他的层次结构与抽象.该模式需要确保该类只能生成一个对象,通常是该类需要消耗较多的资源或者没有多个实例的情况.例如,一个公司只有一个CEO.一个应用只有一个Application对象等.下面以公司里的CEO为例来简单演示一下,一个公司可以有几个VP.无数个员工,但是CEO只有一个,请看下面示例. 示例实现代码 package com.dp.example.singleton; // 普通员工 public class

《Android 源码设计模式解析与实战》——第2章,第2.9节运用单例模式

2.9 运用单例模式 在Android应用开发过程中,ImageLoader是我们最为常用的开发工具库之一.Android中最著名的ImageLoader就是Universal-Image-Loader(https://github.com/nostra13/Android-Universal-Image- Loader),它的使用过程大概是这样的: public void initImageLoader(Context context) { // 1. 使用Builder构建ImageLoad

《Android 源码设计模式解析与实战》——第1章,第1.2节让程序更稳定、更灵活——开闭原则

1.2 让程序更稳定.更灵活--开闭原则 开闭原则的英文全称是Open Close Principle,缩写是OCP,它是Java世界里最基础的设计原则,它指导我们如何建立一个稳定的.灵活的系统.开闭原则的定义是:软件中的对象(类.模块.函数等)应该对于扩展是开放的,但是,对于修改是封闭的.在软件的生命周期内,因为变化.升级和维护等原因需要对软件原有代码进行修改时,可能会将错误引入原本已经经过测试的旧代码中,破坏原有系统.因此,当软件需要变化时,我们应该尽量通过扩展的方式来实现变化,而不是通过修

《Android 源码设计模式解析与实战》——第1章,第1.5节系统有更高的灵活性——接口隔离原则

1.5 系统有更高的灵活性--接口隔离原则 接口隔离原则英文全称是InterfaceSegregation Principles,缩写是ISP.ISP的定义是:客户端不应该依赖它不需要的接口.另一种定义是:类间的依赖关系应该建立在最小的接口上.接口隔离原则将非常庞大.臃肿的接口拆分成更小的和更具体的接口,这样客户将会只需要知道他们感兴趣的方法.接口隔离原则的目的是系统解开耦合,从而容易重构.更改和重新部署. 接口隔离原则说白了就是,让客户端依赖的接口尽可能地小,这样说可能还是有点抽象,我们还是以