概述: 确保一个类只有一个实例, 而且自行实例化并向整个系统提供这个实例.
关键点:
构造函数不对外开放, 一般为 private.
通过一个静态方法或者枚举返回单例类对象.
确保单例类的对象有且只有一个, 尤其在多线程情况下.
确保单例类对象在反序列化时不会重新构建对象
(1) 饿汉模式
饿汉式单例模式 (在类加载时就完成了初始化, 所以类加载较慢, 但获取对象的速度快)
- public class EagerSingle {
- // 饿汉模式单例
- // 在类加载时就完成了初始化, 所以类加载较慢, 但获取对象的速度快
- private static EagerSingle single = new EagerSingle();// 静态私有成员, 已初始化
- private EagerSingle() {
- // 私有构造函数
- }
- public static EagerSingle getInstance() {// 静态, 不用同步 (类加载时已初始化, 不会有多线程的问题)
- return single;
- }
- }
(2) 懒汉模式
懒汉模式声明一个静态对象, 并且在用户第一次调用 getInstance 时进行初始化.
- public class EagerSingleton {
- // 饿汉模式单例
- // 在类加载时就完成了初始化, 所以类加载较慢, 但获取对象的速度快
- private static EagerSingleton instance = new EagerSingleton();// 静态私有成员, 已初始化
- private EagerSingleton() {
- // 私有构造函数
- }
- public static EagerSingleton getInstance() {// 静态, 不用同步 (类加载时已初始化, 不会有多线程的问题)
- return instance;
- }
- }
synchronized 关键字保证了同步, 在多线程情况下单例的唯一性.
存在问题: 即使 instance 已经存在, 每次调用 getInstance 依然会进行同步, 这样就会消耗不必要的资源.
总结: 懒汉模式的优点是只有在使用时才会实例化单例对象, 在一定程度上节约了资源; 缺点是第一次加载时需要进行实例化, 反应稍慢; 最大问题是每次调用都会进行同步吗, 造成不必要的同步开销. 这种模式一般不建议使用.
(3)Double Check Lock(DCL) 双重校验锁
DCL 方式实现单例的优点是既能在需要时才初始化单例, 又能保证线程安全, 且单例对象初始化后调用 getInstance 不进行同步锁.
- public class DCLSingleton {
- //Double Check Lock 单例模式
- // 懒汉模式的改进
- // 但仍然存在隐患
- private static DCLSingleton instance = null;
- private DCLSingleton() {
- }
- public static DCLSingleton getInstance() {
- if (instance == null) {// 第一层判断主要是为了避免不必要的同步
- synchronized (DCLSingleton.class) {
- if (instance == null) {// 第二层判空是为了在 null 情况下创建实例
- instance = new DCLSingleton();
- }
- }
- }
- return instance;
- }
- }
亮点在 getInstance 方法上, 有两次判空. 第一层判断主要是为了避免不必要的同步, 第二层判空是为了在 null 情况下创建实例.
执行下面这行代码
single = new Singleton();
实际上并不是一个原子操作, 这句代码实际做了 3 件事
给 Singleton 的实例分配内存;
调用 Singleton() 的构造函数, 初始化成员字段
将 instance 对象指向分配的内存空间 (此时 instance 已经不是 null 了)
问题: 但由于 java 编译器允许处理器乱序执行, 上述顺序 2,3 是不能保证的, 可能是 1-2-3 也可能是 1-3-2; 如果是后者, 3 执行了已经非空, 再走 2 会出现问题, 这就是 DCL 失效.
解决: volatile 关键字
- // private static DCLSingleton instance = null;
- private volatile static DCLSingleton instance = null;
只需要加上 volatile 关键字, 如上述代码操作就可以保证 instance 对象每次都是从主内存中读取的, 就可以采用 DCL 来完成单例模式了. 当然, volatile 或多或少会影响到性能, 但考虑到程序的正确性, 牺牲点性能还是值得的.
总结:
优点: 资源利用率高, 第一次执行 fetInstance 时单例对象才会被实例化, 效率高.
缺点: 第一次加载时反应稍慢; 由于 java 内存模型的原因偶尔会失败, 在高并发环境下也有一定的缺陷, 虽然概率很小.
DCL 模式是使用最多的单例实现方式
(4) 静态内部类单例模式
- public class InnerSingleton {
- private InnerSingleton() {
- }
- public static InnerSingleton getInstance() {
- return InnerSingletonHolder.instance;
- }
- /**
- * 静态内部类
- */
- private static class InnerSingletonHolder {
- private static final InnerSingleton instance = new InnerSingleton();
- }
- }
总结: 第一次加载 InnerSingleton 类时并不会初始化 instance, 只有在第一次调用 InnerSingleton 的 getInstance 方法时才会导致 instance 被初始化. 因此, 第一次调用 getInstance 方法会导致虚拟机加载 InnerSingleton 类, 这种方法不仅能保证线程安全, 也能够保证单例对象的唯一性, 同时也延迟了单例的实例化, 所以这也是一种推荐的单利模式实现方法
(5) 枚举单例
- public enum EnumSingleton {
- INSTANCE;
- public void doSomething() {
- //do sth ...
- }
- }
震惊? 没错! 就是枚举!
写法简单; 枚举在 java 种与普通类是一样的, 不仅能够有字段, 还能够有自己的方法. 最重要的是默认枚举实例的创建时线程安全的, 并且在任何情况下都是一个单例.
为什么这么说呢? 在上述的集中单例模式实现种, 在一个情况下他们都会出现重新创建对象的情况, 那就是反序列化.
补充: 通过序列化可以将一个单例的实例对象写到磁盘, 然后再读回来, 从而有效的获取一个实例. 即使构造函数是私有的, 反序列化时依然可以通过特殊的途径去创建类的一个新的实例, 相当于调用该类的构造函数.
来源: https://www.cnblogs.com/ivoo/p/10725893.html