云计算百科
云计算领域专业知识百科平台

面试必会:手写单例模式(从零到完美的五种实现)

面试必会:手写单例模式(从零到完美的五种实现)

在设计模式中,单例模式(Singleton Pattern) 是最简单也是最常考的一种。它的核心思想是:确保一个类只有一个实例,并提供一个全局访问点。

在 Java 后端开发的面试中,面试官常常会要求你“手写一个单例”,并在此基础上追问线程安全、指令重排等问题。本文带你逐一击破。

1. 饿汉式(线程安全,简单直接)

“饿汉”的意思是:类加载时就完成了初始化,不管你用不用,先创建出来再说。

public class Singleton {
// 1. 私有化构造方法,防止外部 new
private Singleton() {}

// 2. 内部创建静态实例
private static final Singleton INSTANCE = new Singleton();

// 3. 提供静态获取方法
public static Singleton getInstance() {
return INSTANCE;
}
}

  • 优点:写法简单,类加载时完成实例化,天然线程安全(由 JVM 保证)。
  • 缺点:没有达到懒加载(Lazy Loading)的效果。如果该实例很大且一直没被使用,会造成内存浪费。

2. 懒汉式(线程不安全 -> 线程安全)

“懒汉”指的是:只有在第一次调用 getInstance 时才会去创建实例。

线程不安全版

public class Singleton {
private static Singleton instance;
private Singleton() {}

public static Singleton getInstance() {
if (instance == null) { // 多线程环境下,可能多个线程同时进入这个判断
instance = new Singleton();
}
return instance;
}
}

问题:多线程并发时,可能会创建出多个实例,违背单例初衷。

同步方法版(性能低)

public static synchronized Singleton getInstance() { // 加锁
if (instance == null) {
instance = new Singleton();
}
return instance;
}

问题:虽然解决了线程安全,但每次调用都要加锁,性能开销极大。

双重检查锁定(DCL,面试最常考)

public class Singleton {
// 必须加 volatile 关键字
private static volatile Singleton instance;

private Singleton() {}

public static Singleton getInstance {
// 第一重检查:提升效率,如果已经创建了就不用再进入该同步块
if (instance == null) {
synchronized (Singleton.class) {
// 第二重检查:确认在等待锁的过程中,没有其他线程创建实例
if (instance == null) {
instance = new Singleton();
}
}
}

return instance;
}
}

追问:为什么一定要加 volatile ?

instance = new Singleton 这一行代码在 JVM 中其实分为三步:

  • 分配内存空间。
  • 初始化对象。
  • 将 instance 指向分配的内存地址。
  • 由于 JVM 的指令重排,步骤 2 和 3 的顺序可能会颠倒。如果线程 A 先执行了 1 和 3,此时 instance 已不为 null,但对象初始化还没完成。此时线程 B 进来,第一重检查发现不为 null,直接返回了这个“半成品”对象,程序就会报错。

    volatile 的作用就是禁止指令重排,确保对象创建的有序性。

    4. 静态内部类(推荐方案)

    这是一种更优雅的写法,结合了饿汉式的线程安全和懒汉式的懒加载优点。

    public class Singleton {
    private Singleton() {};

    // 静态内部类只有在被调用时才会被加载
    private static class SingletonHolder {
    private static final Singleton INSTANCE = new Singleton();
    }

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

    原理:只有当调用 getInstance 方法时,才会装在内部类 SingletonHolder,从而触发 INSTANCE 的实例化,由于类加载过程是线程安全的,因此这种方法既实现了延迟加载,又保证了线程安全,代码还非常简介。

    枚举单例(最完美的方案)

    《Effective Java》作者 Josh Bloch 极力推荐的实现方式。

    public enum Singleton {
    INSTANCE;

    public void doSomething() {
    System.out.println("Doing something…");
    }
    }

    调用方式:Singleton.INSTANCE.doSomething;

    为什么最好?

  • 极简:代码最少。
  • 线程安全:由枚举特性保证。
  • 防止反射攻击:即便通过反射调用 setAccessible(true),也无法私自实例化枚举。
  • 防止序列化破坏:枚举自带序列化保护,防止反序列化时生成新实例。
  • 总结:我该选哪种?

    实现方式懒加载线程安全防止反射/序列化攻击推荐等级
    饿汉式 ⭐⭐
    DCL (双重检查) ⭐⭐⭐⭐
    静态内部类 ⭐⭐⭐⭐⭐
    枚举 ⭐⭐⭐⭐⭐

    面试建议:

    • 如果面试官让你写一个“完美的”单例,直接写 静态内部类 或 DCL(记得解释 volatile)。
    • 如果面试官问“哪种方式能防御反射攻击”,一定要提到 枚举。

    如果你觉得这篇文章对你有帮助,欢迎点赞收藏!你的支持是我创作的最大动力。

    赞(0)
    未经允许不得转载:网硕互联帮助中心 » 面试必会:手写单例模式(从零到完美的五种实现)
    分享到: 更多 (0)

    评论 抢沙发

    评论前必须登录!