Android单例模式知识总结

六种核心实现方式对比

1. 饿汉式单例(Eager Initialization)

原理 :利用类加载时静态变量初始化的特性,天然线程安全。
代码

java 复制代码
public class EagerSingleton {
    private static final EagerSingleton INSTANCE = new EagerSingleton();
    private EagerSingleton() {} // 私有构造防止外部实例化
    public static EagerSingleton getInstance() { return INSTANCE; }
}

特点

  • 优点:简单可靠,线程安全,无需额外同步。
  • 缺点:类加载即初始化,浪费内存(适合小资源实例)。
  • 适用场景:程序启动时需初始化的全局配置类。

2. 懒汉式单例(非线程安全 vs 线程安全)

非线程安全(危险)

java 复制代码
public class LazySingleton {
    private static LazySingleton instance;
    public static LazySingleton getInstance() {
        if (instance == null) instance = new LazySingleton(); // 多线程下可能创建多个实例
        return instance;
    }
}

线程安全(同步方法)

java 复制代码
public class SynchronizedLazySingleton {
    private static SynchronizedLazySingleton instance;
    public static synchronized SynchronizedLazySingleton getInstance() {
        if (instance == null) instance = new SynchronizedLazySingleton();
        return instance;
    }
}

特点

  • 优点:延迟初始化,节省内存。
  • 缺点:同步方法性能差(每次调用都加锁),实际开发极少使用。

3. 双重检查锁定(DCL,线程安全)

核心代码

java 复制代码
public class DCLSingleton {
    private static volatile DCLSingleton instance; // volatile 禁止指令重排序
    public static DCLSingleton getInstance() {
        if (instance == null) { // 第一次检查
            synchronized (DCLSingleton.class) { // 同步类对象,缩小锁范围
                if (instance == null) { // 第二次检查
                    instance = new DCLSingleton(); // 非原子操作,需 volatile 保障
                }
            }
        }
        return instance;
    }
}

关键细节

  • volatile 防止指令重排序(如先赋值再初始化导致的空指针风险)。
  • 适用场景:性能敏感且需延迟初始化的场景(如网络管理器)。

4. 静态内部类单例(推荐)

原理:利用静态内部类的类加载机制(JVM 保证线程安全),延迟初始化。

java 复制代码
public class StaticInnerClassSingleton {
    private StaticInnerClassSingleton() {}
    private static class Holder {
        static final StaticInnerClassSingleton INSTANCE = new StaticInnerClassSingleton();
    }
    public static StaticInnerClassSingleton getInstance() {
        return Holder.INSTANCE; // 首次调用时加载 Holder 类,触发实例化
    }
}

特点

  • 兼顾饿汉式的线程安全与懒汉式的延迟初始化,实现优雅。
  • Android 中常用于工具类或轻量管理器。

5. 枚举单例(最安全)

Java 官方推荐(《Effective Java》)

java 复制代码
public enum EnumSingleton {
    INSTANCE; // 枚举常量天然唯一,线程安全
    // 业务方法
    public void doSomething() { /* ... */ }
}

优势

  • 自动支持序列化和反序列化,防止反射攻击(Enum 禁止通过反射创建实例)。
  • 代码极简,无需额外处理线程安全。

6. Kotlin 单例(简洁高效)

伴生对象 + lazy 代理(懒汉式)

Kotlin 复制代码
class KotlinSingleton {
    companion object {
        val instance: KotlinSingleton by lazy { KotlinSingleton() } // 线程安全,延迟初始化
    }
}

枚举实现

Kotlin 复制代码
enum class KotlinEnumSingleton {
    INSTANCE;
    fun doSomething() = Unit
}

优势

  • by lazy 简化 DCL 逻辑,默认线程安全(可配置 LazyThreadSafetyMode)。
  • 枚举语法更简洁,天然防御反射和反序列化。

面试追问

一、基础概念与实现对比类真题

真题 1:饿汉式和懒汉式单例的核心区别是什么?适用场景如何选择?

解答:

  • 核心区别:

    • 初始化时机 :饿汉式在类加载时立即创建实例(静态变量初始化),懒汉式在首次调用 getInstance() 时创建(延迟初始化)。
    • 线程安全 :饿汉式依赖 JVM 类加载机制,天然线程安全;非同步懒汉式多线程下不安全,需通过 synchronized 或 DCL 保证安全。
    • 内存占用:饿汉式无论是否使用都占用内存("饿"),懒汉式节省内存但实现复杂("懒")。
  • 适用场景选择:

    • 若实例占用资源少且希望提前初始化(如全局配置类),选饿汉式(简单可靠)。
    • 若实例创建耗时 / 耗资源(如网络管理器、图片加载引擎),且需延迟初始化,选DCL 懒汉式(兼顾性能与线程安全)。
    • 直接同步方法的懒汉式(synchronized 修饰方法)因性能差,实际开发几乎不用,仅作为概念对比。

考点分析:

考察对两种模式核心原理的理解,需结合 "类加载机制""线程安全实现""性能与内存权衡" 展开,避免仅停留在代码表面。

真题 2:DCL 单例为什么需要 volatile?不加会有什么问题?

解答:

  • 指令重排序风险:
    instance = new DCLSingleton(); 实际分三步:

    1. 分配内存空间(memory = allocate());
    2. 调用构造函数初始化对象(ctorInstance(memory));
    3. 将引用赋值给 instanceinstance = memory)。
      JVM 可能优化为 1→3→2 (重排序),若线程 A 执行到步骤 3 时(instance 非空但未初始化),线程 B 调用 getInstance() 发现 instance 非空,直接返回未初始化的实例,导致空指针异常或逻辑错误。
  • volatile 的作用:

    禁止指令重排序,确保步骤按 1→2→3 执行;同时保证多线程间 instance 的可见性(一个线程修改后,其他线程立即感知)。

反例代码:

若不加 volatile,在高并发下可能返回未初始化的实例,典型面试陷阱!

考点分析:

深入考察 JVM 底层机制(指令重排序、可见性)与多线程安全,需结合底层原理解释,避免仅回答 "防止指令重排序" 的表面原因。

二、Android 特性与实战问题类真题

真题 3:Android 中使用单例时,如何避免内存泄漏?举例说明错误与正确做法。

解答:

  • 错误案例(Activity 上下文泄漏):

    java 复制代码
    public class BadSingleton {
        private Context context; // 持有 Activity 上下文(短生命周期)
        private static BadSingleton instance;
        private BadSingleton(Context context) {
            this.context = context; // 若传入 Activity,Activity 销毁后仍被单例引用,无法回收
        }
        public static BadSingleton getInstance(Context context) {
            if (instance == null) {
                instance = new BadSingleton(context);
            }
            return instance;
        }
    }

    问题: Activity 销毁时,单例仍持有其引用,导致 Activity 无法被 GC 回收,内存泄漏。

  • 正确做法:

    • 使用 Application 上下文 (生命周期与应用一致):

      java 复制代码
      public class GoodSingleton {
          private Context context;
          private static GoodSingleton instance;
          private GoodSingleton(Context context) {
              this.context = context.getApplicationContext(); // 或直接传入 Application
          }
          public static GoodSingleton getInstance(Context context) {
              if (instance == null) {
                  instance = new GoodSingleton(context);
              }
              return instance;
          }
      }
    • 若必须使用 Activity 上下文,确保单例不长期持有(如临时方法参数,而非成员变量)。

考点分析:

结合 Android 组件生命周期(Activity 短生命周期 vs Application 长生命周期),考察内存泄漏的根本原因及预防措施,是 Android 面试必考点。

真题 4:单例模式在 Android 中常用于哪些场景?举 3 个实际例子。

解答:

  1. 全局管理器
    • 网络请求管理器(如 Retrofit 封装类,统一管理 OkHttp 连接池);
    • 数据库助手(如 Room Database 的 DatabaseClient,避免重复创建连接)。
  2. 配置与状态管理
    • 应用主题 / 语言配置中心(存储全局配置,跨页面同步);
    • 用户登录状态管理器(确保各模块获取同一登录状态)。
  3. 轻量工具类
    • 日志工具(如统一的 Logger 类,控制日志级别和输出渠道);
    • Toast 管理类(避免多次创建 Toast 实例,保证显示顺序)。

扩展: 需说明选择单例的原因 ------ 避免资源重复创建、提供全局访问点、维护统一状态,而非简单罗列场景。

三、线程安全与破坏防御类真题

真题 5:单例模式如何防止反射攻击?

解答:

反射可通过 newInstance() 调用私有构造函数创建新实例,破坏单例唯一性。防御措施:

  1. 在构造函数中检查实例是否已存在

    java 复制代码
    private static volatile Singleton instance;
    private Singleton() {
        if (instance != null) { // 防止反射多次创建
            throw new IllegalStateException("Instance already exists");
        }
    }

    注意:首次通过反射创建时,instancenull,可正常创建;再次反射时触发检查。

  2. 枚举单例(终极防御)
    Java 枚举天然防止反射创建实例(Enum.newInstance() 会抛出异常),是最安全的单例实现:

    java 复制代码
    public enum EnumSingleton {
        INSTANCE;
        // 业务方法
    }

考点分析:

考察单例的鲁棒性,需区分普通单例(需手动防御)与枚举单例(天然免疫)的差异,枚举是 Java 官方推荐的安全单例方式

真题 6:反序列化会破坏单例吗?如何解决?

解答:

  • 问题: 反序列化时,ObjectInputStream.readObject() 会创建新对象,导致单例失效。
  • 解决方案:
    1. 添加 readResolve() 方法,返回现有实例:

      java 复制代码
      private Object readResolve() {
          return instance; // 返回单例实例,避免创建新对象
      }
    2. 枚举单例无需额外处理,反序列化时直接返回枚举常量,天然支持单例。

代码示例:

java 复制代码
public class SerializableSingleton implements Serializable {
    private static final long serialVersionUID = 1L;
    private static final SerializableSingleton INSTANCE = new SerializableSingleton();
    private SerializableSingleton() {}
    public static SerializableSingleton getInstance() { return INSTANCE; }
    // 防止反序列化创建新实例
    protected Object readResolve() { return INSTANCE; }
}

考点分析:

考察对 Java 序列化机制的理解,需明确单例在反序列化场景下的漏洞及修复方法,结合 readResolve 的作用深入解释。

相关推荐
xiangxiongfly9151 小时前
Android 圆形和圆角矩形总结
android·圆形·圆角·imageview
幻雨様7 小时前
UE5多人MOBA+GAS 45、制作冲刺技能
android·ue5
Jerry说前后端8 小时前
Android 数据可视化开发:从技术选型到性能优化
android·信息可视化·性能优化
Meteors.9 小时前
Android约束布局(ConstraintLayout)常用属性
android
alexhilton10 小时前
玩转Shader之学会如何变形画布
android·kotlin·android jetpack
whysqwhw14 小时前
安卓图片性能优化技巧
android
风往哪边走14 小时前
自定义底部筛选弹框
android
Yyyy48215 小时前
MyCAT基础概念
android
Android轮子哥15 小时前
尝试解决 Android 适配最后一公里
android
雨白16 小时前
OkHttp 源码解析:enqueue 非同步流程与 Dispatcher 调度
android