单例模式

单例模式在Android中算是很常用的一类模式了,当我们需要整个软件中有且只有一个实例对象时,我们可以写一个单例类。

最简单的单例

public class RecycleBin {
    private static RecycleBin INSTANCE;
    
    private RecycleBin() { }
    
    public static RecycleBin getInstance() {
        if (INSTANCE == null) {
            INSTANCE = new RecycleBin();
        }
        return INSTANCE;
    }

这样的代码大家肯定都不陌生,如果INSTANCE还没有实例化,那么实例化它,并且构造方法是private的,防止客户端new一个实例。这样的代码在单线程环境下可以较好的工作,但是在多线程环境下就会出现错误。假如线程A执行if (INSTANCE == null)这一步后被挂起,线程B切换进来执行了这段代码,实例化了INSTANCE,此时INSTANCE就不为null了,之后把CPU重新让给了线程A,但此时A并不知道INSTANCE已被实例化这件事,又将INSTANCE指向了一个新的RecycleBin对象。

同步的单例模式

那么实现多线程下的单例模式呢。一个很粗暴的答案是:synchronized。是的,给getInstance方法加上这个关键字后,整个方法就是同步的了,可以实现单例模式。

public synchronized static RecycleBin getInstance() {
        if (INSTANCE == null) {
            INSTANCE = new RecycleBin();
        }
        return INSTANCE;
    }

但是这么做会产生新的问题:效率很低。如果有多个线程要同时获得该对象,那么线程需要排队,一个一个获取,这会造成很大的浪费。

改进的单例模式

可以换个角度,因为是在检查INSTANCE == null这一步代码上出现了问题,那么将锁加到这个代码段上即可,将代码修改为

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

DCL(Double Check Locking)

因为synchronized的同步会产生大量的性能开销,追求性能的大佬发明了双重锁的办法来减小开销。

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

比起上面的代码,多了一道检测INSTANCE == null的工序,这行代码可以避免除了第一次以后的同步。当对象已经实例化之后,就不会执行同步代码块了。但是这样的代码还是会存在问题,因为INSTANCE = new RecycleBin()这一行代码不是原子操作
下面来假设一个出错的场景

  1. 线程A执行了getInstance方法
  1. 线程A检查INSTANCE变量,发现为空
  2. 线程A执行INSTANCE == new RecycleBin(),将INSTANCE设置为了非空,但是在构造方法执行前被挂起了
  3. 线程B执行代码,检查INSTANCE变量,发现不为空,返回INSTANCE对象。(但此时INSTANCE还未调用构造方法)

实际上这一行代码被拆成了三步来执行

memory = allocate();            //#1为对象分配内存空间
init(memory);                   //#2初始化
instance = memory;              //#3设置instance,将其指向刚分配的内存空间。

在某些编译器上,2和3会出现倒序,也就是类的域无法得到初始化,从而拿到一个并不正确的对象。
庆幸的是再Java1.5之后的版本可以给INSTANCE加上volitate关键字来避免编译器的优化,拿到正确的对象。

饿汉式

上面的几个方法都是属于懒汉式的方法,即等需要了再去实例化。接下来介绍另一种叫做饿汉式:在类加载时就初始化对象

public class RecycleBin {
    private static RecycleBin INSTANCE = new RecycleBin();
    
    private RecycleBin() { }
    
    public static RecycleBin getInstance() {
        if (INSTANCE == null) {
            INSTANCE = new RecycleBin();
        }
        return INSTANCE;
    }

因为静态域只会加载一次,所以产生的单例对象是安全的。

内部类

public class Singleton {
    // 获得对象实例的方法
    public static Singleton getSingleton() {
        return SingletonHolder.instance;
    }

    /**
     * 静态内部类与外部类的实例没有绑定关系,而且只有被调用时才会
     * 加载,从而实现了延迟加载
     */
    private static class SingletonHolder {
        /**
         * 静态初始化器,由JVM来保证线程安全
         */
        private static Singleton instance = new Singleton();
    }

    private Singleton() {
    }
}

枚举

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

    public void doSomething() {
        // do something
    }
}

使用枚举可以说是最佳的单例实践方式,因为即便构造器是私有的,仍然可以通过反射来调用私有构造器如

public class TestMain {
    public static void main(String[] args) throws NoSuchMethodException, SecurityException, InstantiationException, IllegalAccessException, IllegalArgumentException, InvocationTargetException {
        Class<?> classType = Singleton.class;  
        Constructor<?> c = classType.getDeclaredConstructor(null);  
        c.setAccessible(true);  
        Singleton singleton1 = (Singleton) c.newInstance();  
        Singleton singleton2 = Singleton.getSingleton();  
        System.out.println(singleton1 == singleton2);  
    }
}

另外还有一种特殊情况是反序列化,反序列化并不是通过调用构造器来构造对象的,反序列化操作提供了readSolve方法来重建对象,如果我们要避免反序列化时产生新的对象需要复写这个方法

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

而枚举帮我们完成了这些工作

小结

这部分也只是看书看了个大概,对多线程环境下单例的各种坑并不了解,这部分同时涉及了Java中类的加载机制,多线程,堆,栈等概念。DCL的错误的那部分也看的不是很懂。。这样一个简单的设计模式都有这么多花样,还要学习一个啊。

参考资料

Android设计模式解析与实战
单例模式各版本的原理与实践
Java线程安全兼谈DCL

推荐阅读更多精彩内容

  • 单例模式(SingletonPattern)一般被认为是最简单、最易理解的设计模式,也因为它的简洁易懂,是项目中最...
    huohongsheng阅读 3,446评论 4 34
  • 前言 本文主要参考 那些年,我们一起写过的“单例模式”。 何为单例模式? 顾名思义,单例模式就是保证一个类仅有一个...
    tandeneck阅读 2,013评论 1 8
  • 1 场景问题# 1.1 读取配置文件的内容## 考虑这样一个应用,读取配置文件的内容。 很多应用项目,都有与应用相...
    七寸知架构阅读 5,511评论 12 63
  • 1 单例模式的动机 对于一个软件系统的某些类而言,我们无须创建多个实例。举个大家都熟知的例子——Windows任务...
    justCode_阅读 1,157评论 2 9
  • 1.单例模式概述 (1)引言 单例模式是应用最广的模式之一,也是23种设计模式中最基本的一个。本文旨在总结通过Ja...
    曹丰斌阅读 2,284评论 6 47
  • #146 · 匿名 | 吐槽 1天前 “每次别人说喜欢我 我第一反应就是赶紧拒绝 然后人家没有下一步了 之后我就会...
    山工院表白墙阅读 54评论 0 0
  • 二十多年过去了,越来越深刻得感觉到一直在和自己博弈。 早些时候不太懂事,没有太多压力,没有太难的追求,所以那时没有...
    疯佛阅读 731评论 0 10
  • 今天朋友聚会,一个人说到了抽烟,喝酒对要小孩的影响。他说到抽烟对小孩的影响不大,喝酒对小孩影响大,一问才知道他也是...
    妄_念阅读 31评论 0 0