Android单例模式知识总结

发布于:2025-05-09 ⋅ 阅读:(20) ⋅ 点赞:(0)

六种核心实现方式对比

1. 饿汉式单例(Eager Initialization)

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

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

特点

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

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

非线程安全(危险)

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

线程安全(同步方法)

public class SynchronizedLazySingleton {
    private static SynchronizedLazySingleton instance;
    public static synchronized SynchronizedLazySingleton getInstance() {
        if (instance == null) instance = new SynchronizedLazySingleton();
        return instance;
    }
}

特点

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

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

核心代码

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 保证线程安全),延迟初始化。

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》)

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

优势

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

6. Kotlin 单例(简洁高效)

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

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

枚举实现

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 上下文泄漏):

    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 上下文(生命周期与应用一致):
      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. 在构造函数中检查实例是否已存在
    private static volatile Singleton instance;
    private Singleton() {
        if (instance != null) { // 防止反射多次创建
            throw new IllegalStateException("Instance already exists");
        }
    }
    

    注意:首次通过反射创建时,instance 为 null,可正常创建;再次反射时触发检查。
  2. 枚举单例(终极防御)
    Java 枚举天然防止反射创建实例(Enum.newInstance() 会抛出异常),是最安全的单例实现:
    public enum EnumSingleton {
        INSTANCE;
        // 业务方法
    }
    

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

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

解答:

  • 问题: 反序列化时,ObjectInputStream.readObject() 会创建新对象,导致单例失效。
  • 解决方案:
    1. 添加 readResolve() 方法,返回现有实例:
      private Object readResolve() {
          return instance; // 返回单例实例,避免创建新对象
      }
      
    2. 枚举单例无需额外处理,反序列化时直接返回枚举常量,天然支持单例。

代码示例:

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 的作用深入解释。


网站公告

今日签到

点亮在社区的每一天
去签到