首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Java 设计模式 | 单例模式

Java 设计模式 | 单例模式

作者头像
utopia
发布2023-03-20 17:23:47
3710
发布2023-03-20 17:23:47
举报
文章被收录于专栏:UtopiaUtopia

概述

单例模式,是设计模式中最常见的模式之一,它是一种创建对象模式,用于产生一个对象的具体实例,可以确保系统中一个类只会产生一个实例。

优缺点

优点

  1. 对于频繁使用的对象,可以省去 new 操作花费的时间,尤其对那些重量级对象而言,削减了一笔非常客观的系统开销。
  2. 由于 new 操作的次数减少,因而对系统内存的使用频率也会降低,从而减轻 GC 压力,缩短 GC 停顿时间。

缺点

  1. 单例模式一般没有接口,扩展困难。如果要扩展,则除了修改原来的代码,没有第二种途径,违背开闭原则。
  2. 在并发测试中,单例模式不利于代码调试。在调试过程中,如果单例中的代码没有执行完,也不能模拟生成一个新的对象。
  3. 单例模式的功能代码通常写在一个类中,如果功能设计不合理,则很容易违背单一职责原则。

场景

  • Spring 中创建的 Bean 实例默认都是单例。
  • 数据库连接池的设计与实现。
  • 多线程的线程池设计与实现。

核心结构

单例模式的核心在于通过一个接口返回唯一的对象实例。

image.png
image.png

常见写法

1.饿汉模式

public class Singleton {
​
    private Singleton() {
        System.out.println("create Singleton");
    }
​
    private static Singleton instance = new Singleton();
​
    public static Singleton getInstance(){
        return instance;
    }
}

饿汉模式单例的实现方式简单,在 JVM 对类加载的时候,单例对象就会被创建,因此线程安全。由于获取实例的静态方法没有使用同步方法,调用效率高。但如果该实例从始至终都没被使用过,则会造成内存浪费。

2.懒汉模式

public class LazySingleton {
​
    private LazySingleton() {
        System.out.println("create Singleton");
    }
​
    private static LazySingleton instance = null;
​
    public static synchronized LazySingleton getInstance() {
        if (instance == null) {
            instance = new LazySingleton();
        }
        return instance;
    }
}

懒汉模式单例是对静态成员变量 instance 赋予初始值 null,确保系统启动时没有额外的负载。在第一次使用的时候才进行初始化,达到了懒加载的效果。由于获取实例的静态方法用 synchronized 关键字修饰,所以线程安全。但是由于每次获取实例都要进行同步加锁,因此效率较低。

3.双重检测机制(DCL)

public class DCLSingleton {
​
    private DCLSingleton() {
        System.out.println("create Singleton");
    }
​
    private static volatile DCLSingleton instance = null;
​
    public static DCLSingleton getInstance() {
        if (instance == null) {
            synchronized (DCLSingleton.class) {
                if(instance == null){
                    instance = new DCLSingleton();
                }
            }
        }
        return instance;
    }
}

双重检测机制(双重检查加锁)是在第一次使用的时候才进行初始化,达到了懒加载的效果。在进行初始化的时候会进行同步加锁,因此线程安全。并且只有第一次进行初始化才进行同步,因此不会有效率方面的问题。

CPU 内部会在保证不影响最终结果的前提下对指令进行重新排序(不影响最终结果只是针对单线程,切记),指令重排的主要目的是为了提高效率。

正常情况按顺序执行,双重检测机制是没有问题。如下:

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

指令重排需后:

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

如果线程 A 执行完 1 和 3,instance 对象还未完成初始化,但是已经不再指向 null。此时线程 B 抢占到 CPU 资源,执行第12 行的检测结果为 false,则执行第19行,从而返回一个还未初始化完成的 instance 对象,从而出导致问题出现。

使用 volatile 关键字修饰 instance 对象可以禁止指令重排序。

4.静态内部类

public class StaticInnerHolderSingleton {
​
    private StaticInnerHolderSingleton(){
        System.out.println("create Singleton");
    }
​
    private static class InnerHolder{
        private static StaticInnerHolderSingleton instance = new StaticInnerHolderSingleton();
    }
    
    public static StaticInnerHolderSingleton getInstance(){
        return InnerHolder.instance;
    }
}

当 StaticInnerHolderSingleton 被加载时,内部类 InnerHolder 并不会被初始化,只有在 getInstance() 方法被调用时,才会加载 InnerHolder,从而初始化 instance,做到了延迟加载。

StaticInnerHolderSingleton 实例的创建在 Java 编译时期收集在 () 中,该方法又是同步方法,可以保证内存的可见性、JVM指令的顺序性以及原子性。

5.枚举

public enum EnumSingleton {
​
    INSTANCE;
​
    EnumSingleton(){
        System.out.println("create Singleton");
    }
​
    // 调用getInstance方法,事实上获得Holder的instance静态属性
    public static EnumSingleton getInstance(){
        return INSTANCE;
    }
​

枚举类型不允许被继承,同时是线程安全且只能被实例化一次,但是枚举类型不能够懒加载,对 EnumSingleton 主动使用,如用其中的静态方法 INSTANCE 会立即实例化。

通过 Java 反射机制或序列化和反序列化可能会破坏单例,但枚举模式的单例天然不存在这个问题。

单例破坏问题

通过 Java 反射机制,强行调用单例类的私有构造函数可以生成多个单例示例,这种情况相对极端,代码中也不会去如此实现。

对于序列化和反序列化,可以通过私有方法 readResolve() 解决这个问题,代码如下:

public class SerializableSingleton implements Serializable {
​
    private SerializableSingleton() {
        System.out.println("create Singleton");
    }
​
    private static SerializableSingleton instance = new SerializableSingleton();
​
    public static SerializableSingleton getInstance(){
        return instance;
    }
​
    private Object readResolve(){
        return instance;
    }
}
​

测试代码如下,可以自行测试:

public static void main(String[] args) throws Exception {
        SerializableSingleton s1  = null;
        SerializableSingleton s = SerializableSingleton.getInstance();
        // 先将实例序列化到文件
        FileOutputStream fos = new FileOutputStream("SerializableSingleton.txt");
        ObjectOutputStream oos = new ObjectOutputStream(fos);
        oos.writeObject(s);
        oos.flush();;
        oos.close();
        // 从文件反序列化读出原有的单例类
        FileInputStream fis = new FileInputStream("SerializableSingleton.txt");
        ObjectInputStream ois = new ObjectInputStream(fis);
        s1 = (SerializableSingleton) ois.readObject();
        System.out.println(s.equals(s1));
    }

事实上,在实现了私有的 readResolve() 方法后,readObject() 方法就已经形同虚设,它直接使用 readResolve() 替换了原本的返回值,从而从形式上构造了单例。

总结

在实际工作中,单例的使用还是比较常见的,在几种实现方式中,双重检测机制、静态内部类、枚举方式都是比较推荐。

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2022-08-26 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 概述
  • 优缺点
    • 优点
      • 缺点
      • 场景
      • 核心结构
      • 常见写法
        • 1.饿汉模式
          • 2.懒汉模式
            • 3.双重检测机制(DCL)
              • 4.静态内部类
                • 5.枚举
                • 单例破坏问题
                • 总结
                领券
                问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档