单例模式的几种用法比较

本贴最后更新于 1953 天前,其中的信息可能已经时移俗易

转自## 旭日的芬芳

1.定义

确保某个类只有一个实例,能自行实例化并向整个系统提供这个实例。

2.应用场景

  1. 当产生多个对象会消耗过多资源,比如 IO 和数据操作
  2. 某种类型的对象只应该有且只有一个,比如 Android 中的 Application。

3.考虑情况

  1. 多线程造成实例不唯一。
  2. 反序列化过程生成了新的实例。

4.实现方式

4.1 普通单例模式

/**
 * 普通模式
 * @author josan_tang
 */
public class SimpleSingleton {
    //1.static单例变量
    private static SimpleSingleton instance;

    //2.私有的构造方法
    private SimpleSingleton() {

    }

    //3.静态方法为调用者提供单例对象
    public static SimpleSingleton getInstance() {
        if (instance == null) {
            instance = new SimpleSingleton();
        }
        return instance;
    }
}

在多线程高并发的情况下,这样写会有明显的问题,当线程 A 调用 getInstance 方法,执行到 16 行时,检测到 instance 为 null,于是执行 17 行去实例化 instance,当 17 行没有执行完时,线程 B 又调用了 getInstance 方法,这时候检测到 instance 依然为空,所以线程 B 也会执行 17 行去创建一个新的实例。这时候,线程 A 和线程 B 得到的 instance 就不是一个了,这违反了单例的定义。

4.2 饿汉单例模式

/**
 * 饿汉单例模式
 * @author josan_tang
 */
public class EHanSingleton {
    //static final单例对象,类加载的时候就初始化
    private static final EHanSingleton instance = new EHanSingleton();

    //私有构造方法,使得外界不能直接new
    private EHanSingleton() {
    }

    //公有静态方法,对外提供获取单例接口
    public static EHanSingleton getInstance() {
        return instance;
    }
}

饿汉单例模式解决了多线程并发的问题,因为在加载这个类的时候,就实例化了 instance。当 getInstatnce 方法被调用时,得到的永远是类加载时初始化的对象(反序列化的情况除外)。但这也带来了另一个问题,如果有大量的类都采用了饿汉单例模式,那么在类加载的阶段,会初始化很多暂时还没有用到的对象,这样肯定会浪费内存,影响性能,我们还是要倾向于 4.1 的做法,在首次调用 getInstance 方法时才初始化 instance。请继续看 4.3 用法。

4.3 懒汉单例模式

import java.io.Serializable;

/**
 * 懒汉模式
 * @author josan_tang
 */
public class LanHanSingleton {
    private static LanHanSingleton instance;

    private LanHanSingleton() {

    }

    /**
     * 增加synchronized关键字,该方法为同步方法,保证多线程单例对象唯一
     */
    public static synchronized LanHanSingleton getInstance() {
        if (instance == null) {
            instance = new LanHanSingleton();
        }
        return instance;
    }
}

与 4.1 的唯一区别在于 getInstance 方法前加了 synchronized 关键字,让 getInstance 方法成为同步方法,这样就保证了当 getInstance 第一次被调用,即 instance 被实例化时,别的调用不会进入到该方法,保证了多线程中单例对象的唯一性。

优点:单例对象在第一次调用才被实例化,有效节省内存,并保证了线程安全。

缺点:同步是针对方法的,以后每次调用 getInstance 时(就算 intance 已经被实例化了),也会进行同步,造成了不必要的同步开销。不推荐使用这种方式。

4.4 Double CheckLock(DCL)单例模式

/**
 * Double CheckLock(DCL)模式
 * @author josan_tang
 *
 */
public class DCLSingleton {
    //增加volatile关键字,确保实例化instance时,编译成汇编指令的执行顺序
    private volatile static DCLSingleton instance;

    private DCLSingleton() {

    }

    public static DCLSingleton getInstance() {
        if (instance == null) {
            synchronized (DCLSingleton.class) {
                //当第一次调用getInstance方法时,即instance为空时,同步操作,保证多线程实例唯一
                //当以后调用getInstance方法时,即instance不为空时,不进入同步代码块,减少了不必要的同步开销
                if (instance == null) {
                    instance = new DCLSingleton();
                }
            }
        }
        return instance;
    }
}

DCL 失效:

在 JDK1.5 之前,可能会有 DCL 实现的问题,上述代码中的 20 行,在 Java 里虽然是一句代码,但它并不是一个真正的原子操作。

instance = new DCLSingleton();

它编译成最终的汇编指令,会有下面 3 个阶段:

  1. 给 DCLSingleton 实例分配内存
  2. 调用 DCLSingleton 的构造函数,初始化成员变量。
  3. 将 instance 指向分配的内存空间(这个操作以后,instance 才不为 null)

在 jdk1.5 之前,上述的 2、3 步骤不能保证顺序,也就是说有可能是 1-2-3,也有可能是 1-3-2。如果是 1-3-2,当线程 A 执行完步骤 3(instance 已经不为 null),但是还没执行完 2,线程 B 又调用了 getInstance 方法,这时候线程 B 所得到的就是线程 A 没有执行步骤 2(没有执行完构造函数)的 instance,线程 B 在使用这样的 instance 时,就有可能会出错。这就是 DCL 失效。

在 jdk1.5 之后,可以使用 volatile 关键字,保证汇编指令的执行顺序,虽然会影响性能,但是和程序的正确性比起来,可以忽悠不计。

Java 内存模型

优点:第一次执行 getInstance 时 instance 才被实例化,节省内存;多线程情况下,基本安全;并且在 instance 实例化以后,再次调用 getInstance 时,不会有同步消耗。

缺点:jdk1.5 以下,有可能 DCL 失效;Java 内存模型影响导致失效;jdk1.5 以后,使用 volatile 关键字,虽然能解决 DCL 失效问题,但是会影响部分性能。

4.5 静态内部类单例模式

/**
 * 静态内部类实现单例模式
 * @author josan_tang
 *
 */
public class StaticClassSingleton {
    //私有的构造方法,防止new
    private StaticClassSingleton() {

    }

    private static StaticClassSingleton getInstance() {
        return StaticClassSingletonHolder.instance;
    }

    /**
     * 静态内部类
     */
    private static class StaticClassSingletonHolder {
        //第一次加载内部类的时候,实例化单例对象
        private static final StaticClassSingleton instance = new StaticClassSingleton();
    }
}

第一次加载 StaticClassSingleton 类时,并不会实例化 instance,只有第一次调用 getInstance 方法时,Java 虚拟机才会去加载 StaticClassSingletonHolder 类,继而实例化 instance,这样延时实例化 instance,节省了内存,并且也是线程安全的。这是推荐使用的一种单例模式。

4.6 枚举单例模式

/**
 * 枚举单例模式
 * @author josan_tang
 *
 */
public enum EnumSingleton {
    //枚举实例的创建是线程安全的,任何情况下都是单例(包括反序列化)
    INSTANCE;

    public void doSomething(){

    } 
}

枚举不仅有字段还能有自己的方法,并且枚举实例创建是线程安全的,就算反序列化时,也不会创建新的实例。除了枚举模式以外,其他实现方式,在反序列化时都会创建新的对象。

为了防止对象在反序列化时创建新的对象,需要加上如下方法:

    private Object readResole() throws ObjectStreamException {
        return instance;
    }

这是一个钩子函数,在反序列化创建对象时会调用它,我们直接返回 instance 就是说,不要按照默认那样去创建新的对象,而是直接将 instance 返回。

4.7 容器单例模式

import java.util.HashMap;
import java.util.Map;

/**
 * 容器单例模式
 * @author josan_tang
 */
public class ContainerSingleton {
    private static Map singletonMap = new HashMap();

    //单例对象加入到集合,key要保证唯一性
    public static void putSingleton(String key, Object singleton){
        if (key != null && !"".equals(key) && singleton != null) {
            if (!singletonMap.containsKey(key)) {   //这样防止集合里有一个类的两个不同对象
                singletonMap.put(key, singleton);   
            }
        }
    }

    //根据key从集合中得到单例对象
    public static Object getSingleton(String key) {
        return singletonMap.get(key);
    }
}

在程序初始化的时候,讲多种单例类型对象加入到一个单例集合里,统一管理。在使用时,通过 key 从集合中获取单例对象。这种方式多见于系统中的单例,像安卓中的系统级别服务就是采用集合形式的单例模式,比如常用的 LayoutInfalter,我们一般在 Fragment 中的 getView 方法中使用如下代码:

View view = LayoutInflater.from(context).inflate(R.layout.xxx, null);

其实 LayoutInflater.from(context)就是得到 LayoutInflater 实例,看下面的 Android 源码:

    /**
     * Obtains the LayoutInflater from the given context.
     */
    public static LayoutInflater from(Context context) {
        //通过key,得到LayoutInflater实例
        LayoutInflater LayoutInflater =
                (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
        if (LayoutInflater == null) {
            throw new AssertionError("LayoutInflater not found.");
        }
        return LayoutInflater;
    }

总结

不管是那种形式实现单例模式,其核心思想都是将构造方法私有化,并且通过静态方法获取一个唯一的实例。在获取过程中需要保证线程安全、防止反序列化导致重新生成实例对象。选择哪种实现方式看情况而定,比如是否高并发、JDK 版本、单例对象的资源消耗等。

名称 优点 缺点 备注
简单模式 实现简单 线程不安全
饿汉模式 线程安全 内存消耗太大
懒汉模式 线程安全 同步方法消耗比较大
DCL 模式 线程安全,节省内存 jdk 版本受限、高并发会导致 DCL 失效 推荐使用
静态内部类模式 线程安全、节省内存 实现比较麻烦 推荐使用
枚举模式 线程安全、支持反序列化 个人感觉比较怪异
集合模式 统一管理、节省资源 线程不安全
  • Singleton
    5 引用
  • Java

    Java 是一种可以撰写跨平台应用软件的面向对象的程序设计语言,是由 Sun Microsystems 公司于 1995 年 5 月推出的。Java 技术具有卓越的通用性、高效性、平台移植性和安全性。

    3169 引用 • 8207 回帖

相关帖子

欢迎来到这里!

我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。

注册 关于
请输入回帖内容 ...