深入理解几种单例模式的实现方式

本贴最后更新于 1468 天前,其中的信息可能已经时移世异

饿汉式

饿汉式的单例实现方式就是说在类加载的时候就已经创建并初始化好了,所以实例的创建过程是线程安全的

public class IdGenerator {
    private AtomicLong id = new AtomicLong(0);
    private static final IdGenerator instance = new IdGenerator();
    private IdGenerator(){}
    public static IdGenerator getInstance(){
        return instance;
    }
    public long getId(){
        return id.incrementAndGet();
    }
}

但是饿汉式是有一个缺点的,它不支持延时加载,也就是说实例在使用之前就已经创建好了,如果占用资源过多,但又不使用,会造成一定的浪费,还是需要根据具体情况确定要不要使用这种方式。不过也有人说,如果耗时长,那在使用的时候再加载,就会影响性能,也是难为了饿汉了。

AtomicLong 是一个原子变量类型,提供了线程安全的原子操作,这样可以保证在多线程环境下,获取 id 的时候不会出现重复 ID 的情况。

代码中的构造函数通过 private 修饰符进行修饰,保证了外部代码不能通过构造函数初始化 IdGenerator 类。

上面是一个简单的唯一递增 ID 号码生成器,采用的饿汉式的单例实现模式,instance 实例定义成了一个静态常量,我们这知道在运行一个类的时候,先要加载到 JVM 中,类加载的过程分为三个阶段,分别是:加载、链接和初始化,其中链接阶段又分为三个步骤分别是:验证、准备和解析,其中在准备的步骤中就会创建类或接口的静态变量,并初始化静态变量的初始值。

懒汉式

上面我们说到饿汉式是不支持延时加载到的,那懒汉式就支持延时加载了,懒汉式的实现方式是给获取实例的方法加了锁

public class IdGenerator {
    private AtomicLong id = new AtomicLong(0);
    private static IdGenerator instance;
    private IdGenerator(){}
    
    public static synchronized IdGenerator getInstance(){
        if (instance == null){
            instance = new IdGenerator();
        }
        return instance;
    }
    public long getId(){
        return id.incrementAndGet();
    }
}

加锁的结果就是性能降低,如果这个单例被频繁的使用的话,那性能问题就会比较严重,需要考虑换种方式实现了。

双重检测

双重检测的单例实现方式弥补了上面饿汉式和懒汉式的缺点:不能延时加载和性能低的问题,具体实现方式就是在获取的实例的时候先判断是否已经创建过了,如果是就直接返回,这是第一重检测,没有的话,就进入同步块,进入同步块后再进行判断实例是否已经存在,如果存在就直接返回,这是第二重检测,如果不存在的话就在同步的情况下创建一个实例。

public class IdGenerator {
    private AtomicLong id = new AtomicLong(0);
    private static IdGenerator instance;
    private IdGenerator(){
        // 初始化代码
    }
    public static IdGenerator getInstance(){
        if (instance == null){
            synchronized (IdGenerator.class){ // 这里指明synchronized保护的是当前的类对象
                if (instance == null){
                    instance = new IdGenerator();
                }
            }
        }
        return instance;
    }
    public long getId(){
        return id.incrementAndGet();
    }
}

懒汉式的实现方式每次获取实例的时候都要进同步代码,这样就会造成多次的获取锁释放锁,造成性能损耗,但是双重检测实际上只需要同步一次创建实例就可以了,再获取实例的时候是不用进同步块代码的,这样就大大提高了性能。

静态内部类

静态内部类的实现方式是一种比双重检测更加简单的一种实现方式,而且既保证了性能又做到了延时加载

public class IdGenerator {
    private AtomicLong id = new AtomicLong(0);
    private IdGenerator(){
        // 初始化代码
    }
    private static class SingletonHolder{
        private static final IdGenerator instance = new IdGenerator();
    }
    
    public static IdGenerator getInstance(){
        return SingletonHolder.instance;
    }
    public long getId(){
        return id.incrementAndGet();
    }
}

SingletonHolder 是一个静态内部类,使用 privat 可以使内部类完全对外隐藏,当外部类 IdGenerator 加载的时候,并不会创建实例,只有当调用 getInstance 方法的时候才会加载 SingletonHolder,并创建实例,这样就具备了饿汉式的安全特性,同时也具备了延迟加载的特性。

枚举

枚举的实现方式是利用了 java 枚举类型本身的特性,保证了实例创建的线程安全和实例的唯一性

public enum IdGenerator {
    INSTANCE;
    private AtomicLong id = new AtomicLong(0);
    public long getId(){
        return id.incrementAndGet();
    }
    public static void main(String[] args) {
        IdGeneratorEnum.INSTANCE.getId();
    }
}

可以看出枚举的实现方式是最简洁的,由 jvm 保证线程安全和单一实例。还可以有效防止序列化和反序列化造成多个实例和利用反射创建多个实例的情况。枚举类型在编译成 class 文件后,再反编译,

public final class IdGenerator extends java.lang.Enum<IdGenerator> {
  public static final IdGenerator INSTANCE;
  public static IdGenerator[] values();
  public static IdGenerator valueOf(java.lang.String);
  public long getId();
  public static void main(java.lang.String[]);
  static {};
}

反编译后的枚举类其实继承了 Enum 这个类,INSTANCE 是一个静态常量了,似乎又回到了饿汉式的那种那些,但是比它多出了延迟加载而且更加简洁的特性

  • 测试开发
    8 引用 • 5 回帖
  • Java

    Java 是一种可以撰写跨平台应用软件的面向对象的程序设计语言,是由 Sun Microsystems 公司于 1995 年 5 月推出的。Java 技术具有卓越的通用性、高效性、平台移植性和安全性。

    3167 引用 • 8207 回帖
  • 设计模式

    设计模式(Design pattern)代表了最佳的实践,通常被有经验的面向对象的软件开发人员所采用。设计模式是软件开发人员在软件开发过程中面临的一般问题的解决方案。这些解决方案是众多软件开发人员经过相当长的一段时间的试验和错误总结出来的。

    198 引用 • 120 回帖 • 2 关注

相关帖子

欢迎来到这里!

我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。

注册 关于
请输入回帖内容 ...