Skip to content

Latest commit

 

History

History
234 lines (139 loc) · 5.46 KB

单例模式.md

File metadata and controls

234 lines (139 loc) · 5.46 KB

单例模式

介绍

单例对象的类必须保证只有一个实例存在。

定义

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

使用场景

确保某个类有且只有一个对象的场景,避免产生多个对象消耗过多的资源,或者某种型号的对象只应该有且只有一个。

UML

pic

关键点

  1. 构造函数不对外开放,一般为private

  2. 通过一个静态方法或者枚举返回单例类对象

  3. 确保单例类的对象有且只有一个,尤其是在多线程环境下

  4. 确保单例类对象在反序列化时不会重新构建对象

代码实现

饿汉式

public class Singleton1 {

    private static Singleton1 sInstance = new Singleton1();

    private Singleton1() {

    }

    public static Singleton1 getInstance() {
        return sInstance;
    }

}

优点

  • 线程安全、绝对单例

缺点

  • 会浪费内存

  • 无法防止反序列化以及反射产生新的实例

懒汉式

public class Singleton2 {

    private static Singleton2 sInstance;

    private Singleton2() {

    }

    public static Singleton2 getInstance() {
        if (sInstance == null) {
            sInstance = new Singleton2();
        }
        return sInstance;
    }

}

优点

  • 使用时才加载,节省内存

缺点

  • 线程不安全:在多线程情况下,可能出现多个线程同时实例化对象

  • 无法防止反序列化以及反射产生新的实例

Double Check Lock(DCL)实现单例

public class Singleton3 {

    private static Singleton3 sInstance;

    private Singleton3() {
    }

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

}

第一个if语句的作用是避免不必要的同步,第二if语句的作用是在null的情况下创建实例。

优点

  • 资源利用率高,大部分情况下线程安全

缺点

  • 线程并非完全安全

  • 无法防止反序列化以及反射产生新的实例

  • 代码丑陋

线程不安全的原因:

sInstance = new Singleton()这个语句并不是一个原子操作,最终会被编译成多条汇编指令,大致做了三件事:

  1. 给Singleton的实例分配内存

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

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

但是,由于Java编译器允许处理器乱序执行,以及JDK1.5之前JMM(Java Memory Model,即Java内存模型)中Cache、寄存器到主内存会写顺序的规定,上面的2、3的顺序是无法保证的。也就是说执行顺序可能是1-2-3,也可能是1-3-2。这个时候DCL就会失效,有可能获取到没有初始化的对象。

DCL强化版

只需要将sInstance的定义改为如下这样:

private volatile static Singleton3 sInstance;

这样就可以保证sInstance每次都是从主内存中读取。(关于volatile的详细用法请看这篇文章

但是,volatile会影响性能。

静态内部类单例

public class Singleton4 {

    private Singleton4() {
    }

    public static Singleton4 getInstance() {
        return SingletonHolder.sInstance;
    }

    private static class SingletonHolder {
        private static final Singleton4 sInstance = new Singleton4();
    }

}

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

JVM在类的初始化阶段(即在Class被加载后,且被线程使用之前),会执行类的初始化。在执行类的初始化期间,JVM会去获取一个。这个锁可以同步多个线程对同一个类的初始化。

优点

  • 线程安全

线程安全原因:Java的运行机制保证了static修饰的类、对象仅被初始化一次。

缺点

  • 无法防止反序列化以及反射产生新的实例

枚举单例

public enum Singleton5 {

    INSTANCE;

}

优点

  • 简单

  • 线程安全

  • 防止反序列化、反射产生新的对象

缺点

  • 内存浪费(与饿汉模式类似)

使用容器实现单例模式

public class SingletonManager {

    private static Map<String, Object> objMap = new HashMap<String, Object>();

    private SingletonManager() {

    }

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

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

}

在程序的初始,将多种单例类型注入到一个统一的管理类中,在使用时根据key获取对象对应类型的对象。 这种方式使得可以管理多种类型的单例,并且在使用时可以通过统一的接口进行获取操作。