设计模式之单例模式

1 单例模式简介

1.1 介绍

单例模式应该是日常使用最为广泛的一种模式了。他的作用是确保某个类只有一个实例,避免产生多个对象消耗过多的资源。比如对数据库的操作时;在一个应用中只有一个ImageLoader实例,这个ImageLoader又含有线程池、缓存系统、网络请求等,很消耗资源,就可以使用单例模式。

1.2 单例模式定义

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

1.3 使用场景

避免某个类产生多个对象而消耗过多的资源,确保某个类在程序中只有一个实例。比如我们使用的图片加载器ImageLoader。往往单例创建的对象,耗费的资源都比较多,所以在初始化单例对象的时候就显得尤为重要了,接下来,我们就来聊一聊单例的几种实现方式。

1.4 使用单例模式需要注意的关键点

(1)将构造函数访问修饰符设置为private
(2)通过一个静态方法或者枚举返回单例类对象
(3)确保单例类的对象有且只有一个,特别是在多线程环境下
(4)确保单例类对象在反序列化时不会重新构建对象

2 单例模式UML类图

这里写图片描述

3 单例模式的六种写法

3.1 饿汉式

/**
 * 饿汉式实现单例模式
 */
public class Singleton {
    private static Singleton instance = new Singleton();

    private Singleton() {
    }

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

饿汉式顾名思义,就是这个汉子很饿,一上来就把单例对象创建出来了,要用的时候直接返回即可,这种是单例模式中最简单的一种实现方式。但是问题也比较明显。单例在还没有使用到的时候,初始化就已经完成了。如果程序从头到位都没用使用这个单例的话,单例的对象还是会创建,这就造成了不必要的资源浪费。

3.2 懒汉式

/**
 * 懒汉式实现单例模式
 */
public class Singleton {
    private static Singleton instance;

    private Singleton() {
    }

    // synchronized方法,多线程情况下保证单例对象唯一
    public static synchronized Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}

(1)饿汉式也顾名思义,就是这个汉子比较懒,一开始的时候什么也不做,知道要使用的时候采取创建实例的对象。getInstance()方法中添加了synchronized关键字,使其变成一个同步方法,目的是为了在多线程环境下保证单例对象唯一。
(2)优点: 只有在使用时才会实例化单例,一定程度上节约了资源。
(3)缺点: 第一次加载时要立即实例化,反应稍慢。每次调用getInstance()方法都会进行同步,这样会消耗不必要的资源。这种模式一般不建议使用。

3.3 DCL(Double CheckLock)实现单例(常用)

3.3.1 DCL实现单例模式

**
 * DCL实现单例模式
 */
public class Singleton {
    private volatile static Singleton instance = null;

    private Singleton() {
    }

    public static Singleton getInstance() {
        // 两层判空,第一层是为了避免不必要的同步
        // 第二层是为了在null的情况下创建实例
        if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

(1)优点: 资源利用率高,既能够在需要的时候才初始化实例,又能保证线程安全,同时调用getInstance()方法不进行同步锁,效率高。
(2)缺点: 第一次加载时稍慢,由于Java内存模型的原因偶尔会失败。在高并发环境下也有一定的缺陷,虽然发生概率很小。
因此添加一个volatile关键字,因为在这里会有DCL失效问题,原因是编译器为了提高执行效率的指令重排。只要认为在单线程下是没问题的,它就可以进行乱序写入!以保证不要让cpu指令流水线中断。

3.3.2 JVM的底层机制和执行流程

这里写图片描述
这里写图片描述

3.3.3 原因分析

在3.3.1 DCL实现单例模式中的instance = new Singleton();到底发生了什么?

memory = allocate();   //1:分配对象的内存空间
ctorInstance(memory);  //2:初始化对象
instance = memory;     //3:设置instance指向刚分配的内存地址

上面的伪代码中2、3步重排序之后的执行时序如下:

memory = allocate();   //1:分配对象的内存空间
instance = memory;     //3:设置instance指向刚分配的内存地址
                       //注意,此时对象还没有被初始化!
ctorInstance(memory);  //2:初始化对象

为了更好的理解intra-thread semantics(线程内语义),请看下面的示意图(假设一个线程A在构造对象后,立即访问这个对象)。只要保证2排在4的前面,即使2和3之间重排序了,也不会违反intra-thread semantics。
在这里插入图片描述
下面,再让我们看看多线程并发执行的时候的情况。如果发生重排序,另一个并发执行的线程B就有可能在第4行判断instance不为null。线程B接下来将访问instance所引用的对象,但此时这个对象可能还没有被A线程初始化!下面是这个场景的具体执行时序:
这里写图片描述
最后,线程有可能得到一个不为null,但是构造不完全的instance对象(没经过:2.初始化对象步骤)。

3.3.4 解决方法

(1)不允许2和3重排序。在JDK1.5之后,具体化了volatile关键字,只要定义时加上他,可以保证执行的顺序,虽然会影响性能。这种方式第一次加载时会稍慢,在高并发环境会有缺陷,但是一般能够满足需求。
(2)方官方比较推荐的一种方案(effective java 2nd),请看下一节:3.4 静态内部类(推荐)。

3.3.5 参考链接

双重检查锁定与延迟初始化

3.4 静态内部类(推荐)

/**
 * 静态内部类实现单例模式
 */
public class Singleton {
    private Singleton() {
    }

    public static Singleton getInstance() {
        return SingletonHolder.instance;
    }

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

因为内部静态类是要在有引用之后才会转载到内存的,所以第一次加载Singleton类时不会初始化instance,只有在第一次调用getInstance()方法时,虚拟机会加载SingletonHolder类初始化instance,保证了单例对象的唯一。
因为getInstance方法并没有被同步,并且只是执行一个域的访问,因此延迟初始化并没有增加任何访问成本,实现了懒加载。因为静态的域只会在虚拟机转载类似初始化一次,并有虚拟机保证线程安全。

3.5 枚举单例

/**
 * 枚举实现单例模式
 */
public enum SingletonEnum {
    INSTANCE;
    public void doSomething() {
        System.out.println("do something");
    }
}

默认枚举实例的创建是线程安全的,即使反序列化也不会生成新的实例,任何情况下都是一个单例。优点: 简单!

3.6 容器实现单例

/**
 * 容器类实现单例模式
 */
public class SingletonManager {
    private static Map<String, Object> objMap = new HashMap<String, Object>();

    public static void regsiterService(String key, Object instance) {
        if (!objMap.containsKey(key)) {
            objMap.put(key, instance);
        }
    }

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

SingletonManager可以管理多个单例类型,使用时根据key获取对象对应类型的对象。这种方式可以通过统一的接口获取操作,隐藏了具体实现,降低了耦合度。

4 防止单例模式被JAVA反射攻击

(1)举例,通过JAVA的反射机制来“攻击”单例模式

public class ElvisReflectAttack{
 
    public static void main(String[] args) throws InstantiationException, IllegalAccessException, IllegalArgumentException, InvocationTargetException, NoSuchMethodException, SecurityException {
        Class<?> classType = Singleton.class;
        Constructor<?> c = classType.getDeclaredConstructor(null);
        c.setAccessible(true);
        Elvis e1 = (Singleton)c.newInstance();
        Elvis e2 = Singleton.getInstance();
        
        System.out.println(e1==e2);
    }
}
// 运行结果:false

小结:通过反射获取构造函数,然后调用setAccessible(true)就可以调用私有的构造函数,所有e1和e2是两个不同的对象。如果要抵御这种攻击,可以修改构造器,让它在被要求创建第二个实例的时候抛出异常。
(2)以DCL实现单例模式为例

/**
 * DCL实现单例模式
 */
public class Singleton {
    private volatile static Singleton instance = null;
    private static boolean flag = false;

    private Singleton() {
        synchronized(Singleton.class) {
            Log.e("Interview_log", "flag:" + flag);
            if(!flag) {
                flag = true;
            } else {
	            //在被要求创建第二个实例的时候抛出异常,无法生成对象实例
                throw new RuntimeException("单例模式被侵犯!");
            }
        }
    }

    public static Singleton getInstance() {
        // 两层判空,第一层是为了避免不必要的同步
        // 第二层是为了在null的情况下创建实例
        if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

(3)测试demo

private void reflectAttack() {
        Class<?> classType = Singleton.class;
        Singleton singleton1 = Singleton.getInstance();
        Constructor<?> c = null;

        try {
            c = classType.getDeclaredConstructor();
            c.setAccessible(true);
            Singleton singleton2 = null;
            try {
                singleton2 = (Singleton) c.newInstance();//反射得到的对象
            } catch (Exception e) {
                Log.e("Interview_log", "Exception:" + e.toString());
                e.printStackTrace();
            }
            
            Log.e("Interview_log", "反射攻击2:" + singleton1.toString());
            if (singleton2 == null) {
                Log.e("Interview_log", "反射攻击3:反射得到的对象为null");
            } else {
                Log.e("Interview_log", "反射攻击3:" + singleton2.toString());
            }
            Log.e("Interview_log", "反射攻击4:" + (singleton1 == singleton2));
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        }
    }

(3)结果
这里写图片描述
(4)学习链接
如何防止单例模式被JAVA反射攻击

(5)经过验证,枚举单例可以防止单例模式被“攻击”,也可以防止序列化破坏单例模式。

5 Android源码中的单例模式

在Android系统中,我们经常会通过Context获取系统级别的服务,如WindowsManagerService、ActivityManagerService等,更常用的是一个LayoutInflater的类,这些服务会在合适的时候以单例的形式注册在系统中,在我们需要的时候就通过Context的getSystemService(String name)获取。

6 运用单例模式

7 总结

7.1 优点

(1)由于单例模式在内存中只有一个实例,减少了内存开支,特别是一个对象需要频繁的创建、销毁时,而且创建或销毁时性能又无法优化,单例模式的优势就非常明显。
(2)单例模式可以避免对资源的多重占用,例如一个文件操作,由于只有一个实例存在内存中,避免对同一资源文件的同时操作。
(3)单例模式可以在系统设置全局的访问点,优化和共享资源访问,例如,可以设计一个单例类,负责所有数据表的映射处理。

7.2 缺点

(1)单例模式一般没有接口,扩展很困难,若要扩展,只能修改代码来实现。
(2)单例对象如果持有Context,那么很容易引发内存泄露。此时需要注意传递给单例对象的Context最好是Application Context。

8 参考文章与链接

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

《设计模式之禅》

单例模式的6种实现方式

单例模式的五种实现方式

DCL失效问题的探讨

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值