HarmonyOS Next面试题之主线程与子线程访问同一个单例,获取的对象是同一个吗?

一、前言

  • 在传统的多线程编程中,单例模式意味着"全局唯一"。但当主线程与子线程同时访问这个单例时,它们拿到的是同一个对象吗?在 HarmonyOS NEXT 中,答案可能出乎你的意料。
  • 假设现在有一个非常经典的单例类:
javascript 复制代码
export class Singleton {
  private static instance: Singleton | null = null;
  public value: number = 0;

  private constructor() {}

  static getInstance(): Singleton {
    if (Singleton.instance === null) {
      Singleton.instance = new Singleton();
    }
    return Singleton.instance;
  }
}
  • 在主线程中,获取这个单例,并修改其 value 为 100:
javascript 复制代码
const mainSingleton = Singleton.getInstance();
mainSingleton.value = 100;
  • 然后,我们通过 TaskPool 开启一个子线程,同样获取这个单例,修改 value 为 999,并打印结果:
javascript 复制代码
@Concurrent
function getSingletonValue(): number {
  const workerSingleton = Singleton.getInstance();
  workerSingleton.value = 999;
  return workerSingleton.value;
}
  • 主线程中的 mainSingleton.value 会变成 999 吗?换句话说,主线程和子线程获取到的"单例"是同一个对象吗?直觉告诉我们,单例模式保证全局唯一,当然应该是同一个。但在 HarmonyOS NEXT 中,答案是否定的。

二、原因分析

① 传统模型的"理所当然"

  • 在 Java / Android 等传统共享内存并发模型中,所有线程共享同一块堆内存。静态变量 instance 存放在方法区(或堆中),所有线程看到的都是同一个引用。因此,无论哪个线程调用 getInstance(),拿到的都是同一个对象。
  • 为了保证线程安全,通常需要加锁(synchronized)或使用双重检查锁。

② HarmonyOS NEXT 的"另辟蹊径"------ Actor 模型

  • HarmonyOS NEXT 的 ArkTS 语言并没有采用传统的共享内存并发模型,而是选择了 Actor 模型。
    • 每个线程(包括主线程、TaskPool 工作线程、Worker 线程)拥有一个完全独立的 ArkTS 引擎实例。
    • 不同线程之间的内存是完全隔离的 ------ 它们不共享堆内存,也无法直接访问对方的变量。
    • 跨线程通信的唯一方式是"消息传递",默认采用 结构化克隆算法(Structured Clone),也就是深拷贝,而非传递引用。
  • 这意味着:
    • 主线程中的静态变量 Singleton.instance 只存在于主线程的引擎中。
    • 子线程(TaskPool / Worker)启动时,会初始化自己的一套运行环境,它也有自己的全局对象,其中的 Singleton.instance 初始为 null。
    • 当子线程第一次调用 Singleton.getInstance() 时,它会重新创建一个新的 Singleton 对象,并存储到子线程自己的静态变量中。
  • 因此,主线程和子线程各自持有完全独立的单例实例,互不干扰。

③ 为什么不共享内存?

  • Actor 模型的设计目标是为了彻底避免锁竞争、死锁、数据竞态等传统并发编程的痛点。每个 Actor 独立处理自己的消息,状态不会意外被其他线程修改。这虽然牺牲了"直接共享对象"的便利性,但换来了极高的并发安全性,尤其适合 UI 与后台任务频繁交互的场景。

三、实例分析

  • 如下所示,是一个完整的示例,运行在 HarmonyOS NEXT 环境下(API 12+):
javascript 复制代码
// Singleton.ets
export class Singleton {
  private static instance: Singleton | null = null;
  public value: number = 0;

  private constructor() {}

  static getInstance(): Singleton {
    if (Singleton.instance === null) {
      Singleton.instance = new Singleton();
    }
    return Singleton.instance;
  }
}
javascript 复制代码
// Index.ets
import { taskpool } from '@kit.ArkTS';
import { Singleton } from './Singleton';

@Concurrent
function getSingletonValue(): number {
  const s = Singleton.getInstance();
  s.value = 999;
  console.info(`[Worker] 设置 value = ${s.value}`);
  return s.value;
}

@Entry
@Component
struct Index {
  async onPageShow() {
    // 主线程获取单例,设置 value = 100
    const mainSingleton = Singleton.getInstance();
    mainSingleton.value = 100;
    console.info(`[Main] 主线程实例 value = ${mainSingleton.value}`);

    // 子线程执行任务
    const task = new taskpool.Task(getSingletonValue);
    const result = await taskpool.execute(task) as number;

    console.info(`[Main] 子线程返回的 value = ${result}`);
    console.info(`[Main] 当前主线程实例 value = ${mainSingleton.value}`);
  }

  build() {
    Column() {
      Text("HarmonyOS NEXT 单例测试").fontSize(20)
    }
  }
}
  • 运行结果如下:
javascript 复制代码
[Main] 主线程实例 value = 100
[Worker] 设置 value = 999
[Main] 子线程返回的 value = 999
[Main] 当前主线程实例 value = 100
  • 子线程将单例的 value 改为 999,但主线程的 value 仍然是 100,纹丝不动。如果它们是同一个对象,主线程的 value 也应该变成 999。因此,主线程和子线程获得的"单例"不是同一个对象。
javascript 复制代码
// SharedModule.ets
"use shared" // 关键标记
import { ArkTSUtils } from '@kit.ArkTS';

@Sendable // 关键装饰器
export class TaskHandle {
  private static instance: TaskHandle = new TaskHandle();
  // ... 你的单例逻辑 ...
  static getInstance(): TaskHandle {
    return TaskHandle.instance;
  }
}

四、子线程访问主线程"单例"对象的方法

① 共享模块(推荐方案)

  • 原理:通过标记 "use shared" 和 @Sendable,指定一个模块为共享模块。系统会为所有线程创建并共享该模块中导出变量的同一份实例。
  • 使用方式:将单例类定义在.ets文件顶部添加"use shared"的共享模块中。
javascript 复制代码
"use shared" // 关键标记
import { ArkTSUtils } from '@kit.ArkTS';

@Sendable // 关键装饰器
export class TaskHandle {
  private static instance: TaskHandle = new TaskHandle();
  // ... 你的单例逻辑 ...
  static getInstance(): TaskHandle {
    return TaskHandle.instance;
  }
}
  • 这种方式是官方推荐方案,语法简洁,但是需要要求 API 12 及以上,且 @Sendable 类有较多限制(如成员变量类型需为基本类型或 @Sendable 类型)。

② Sendable共享对象

  • 原理:若不需要完整单例,只想共享一个对象实例,可使用 @Sendable 装饰器。
  • 使用方式:将需要共享的对象标记为 @Sendable。系统会在跨线程传递时传递该对象的引用,而非副本。

③ 共享内存(SharedArrayBuffer)

  • 原理:对于大数据共享场景,SharedArrayBuffer 允许不同线程访问同一块内存区域。
  • 使用方式:创建一个 SharedArrayBuffer 对象,并在主线程和子线程中操作同一块内存。
  • 优点:性能高,无序列化开销。
  • 缺点:操作复杂,需配合 Atomics API 进行原子操作。

④ 消息传递与数据副本

  • 原理:最直接的方式。子线程通过 TaskPool.execute() 接收主线程的数据,并将结果返回。
  • 适用场景:适合逻辑独立的任务,如计算、文件处理。
  • 限制:数据量大时序列化成本较高。

五、如何实现真正的跨线程共享单例?

  • 默认行为:
    • 普通单例类:主线程与子线程各自拥有独立的实例,互不影响。
    • 根本原因:Actor 模型带来的线程内存隔离 + 结构化克隆。
    • 适用场景:如果你希望子线程的修改不影响主线程(例如纯计算任务),这种隔离反而是优点。
  • 如果确实需要主线程与子线程操作同一个对象,HarmonyOS NEXT 提供了官方的解决方案:共享模块 + @Sendable 装饰器。步骤如下:
    • 在 .ets 文件顶部添加 "use shared" 指令。
    • 使用 @Sendable 装饰类。
    • 正常编写单例逻辑。
javascript 复制代码
// SharedSingleton.ets
"use shared"   // 必须放在最顶部

import { ArkTSUtils } from '@kit.ArkTS';

@Sendable
export class SharedSingleton {
  private static instance: SharedSingleton = new SharedSingleton();
  public value: number = 0;

  private constructor() {}

  static getInstance(): SharedSingleton {
    return SharedSingleton.instance;
  }
}
  • 此时,无论在哪个线程调用 SharedSingleton.getInstance(),返回的都是同一个对象,修改 value 会立即在所有线程中可见。
  • 需要注意的是:
    • 要求 API 12 及以上。
    • @Sendable 类有较多限制(成员变量必须为基本类型、@Sendable 类型或某些特定类型)。
    • 跨线程共享虽然方便,但需要自己保证线程安全(例如使用 Atomics 或消息串行化)。
  • SharedArrayBuffer:适合大块数据的共享(如二进制数据),需要配合 Atomics 进行同步。消息传递 + 数据副本:最简单,适合数据量小、逻辑独立的场景。
相关推荐
南村群童欺我老无力.4 小时前
鸿蒙PC开发的@Builder函数闭合大括号的隐形杀手
华为·harmonyos
花先锋队长4 小时前
从静态到“AI动态”:华为Pura X Max独家首发AI动态漫画再创阅读新体验
科技·华为·harmonyos
Lanren的编程日记4 小时前
Flutter 鸿蒙应用错误处理优化实战:完善全局异常捕获,全方位提升应用稳定性
flutter·华为·harmonyos
Lanren的编程日记4 小时前
Flutter鸿蒙应用开发:网络请求优化实战,全方位提升请求稳定性与性能
网络·flutter·harmonyos
IntMainJhy5 小时前
【futter for open harmony】Flutter 鸿蒙聊天应用实战:shared_preferences 本地键值存储适配指南[特殊字符]
flutter·华为·harmonyos
IntMainJhy5 小时前
【Flutter for OpenHarmony 】第三方库鸿蒙电商实战|首页模块完整实现[特殊字符]
flutter·华为·harmonyos
Lanren的编程日记6 小时前
Flutter 鸿蒙应用离线模式实战:无网络也能流畅使用
网络·flutter·harmonyos
IntMainJhy6 小时前
【Flutter for OpenHarmony 】第三方库 聊天应用:Provider 状态管理实战指南
flutter·华为·harmonyos
想你依然心痛7 小时前
HarmonyOS 6金融应用实战:基于悬浮导航与沉浸光感的“光影财富“智能投顾系统
金融·harmonyos·鸿蒙·悬浮导航·沉浸光感