Android 协程时代,出现 ReentrantLock 就是架构警报

0 阅读3分钟

在协程成为主流之后,我越来越坚定一个观点:

在现代 Android / Kotlin 项目中,
不要在业务层显式使用 AQS 同步器。

包括:

  • ReentrantLock
  • CountDownLatch
  • Semaphore
  • FutureTask
  • synchronized

不是因为它们不好。

而是因为——

它们属于更低一层的抽象。


一、什么叫“显式使用 AQS”?

不是指你写了多复杂的并发代码。

而是指:

在 Data 层 / UseCase 层直接操作线程同步器。

比如在 UseCase 里:

image.png

或者在 Repository 里:

image.png

在协程时代,这类代码基本可以判断为:

并发抽象层级发生错位。

问题不在语法。
问题在抽象。


二、问题不在“对错”,而在“层级”

AQS(AbstractQueuedSynchronizer)解决的是:

  • 线程竞争
  • 线程阻塞
  • 等待队列管理
  • 线程级调度

它属于 JVM 并发模型的底层能力。

而你现在在用什么?

👉 协程。

协程的设计哲学是:

  • 挂起,而不是阻塞
  • 结构化并发
  • 任务级调度
  • 线程与任务解耦

当你在协程架构中使用 ReentrantLock,本质是在:

用线程模型解决任务模型的问题。

这就像在 Compose 里手动操作 View。

不是不能。

而是违背抽象边界。


三、线程阻塞 vs 协程挂起

来看最本质的差别。

ReentrantLock 的行为:

  1. 抢不到锁
  2. 阻塞线程
  3. 进入等待队列
  4. 被唤醒

Mutex 的行为:

  1. 抢不到锁
  2. 挂起协程
  3. 不占用线程
  4. 恢复 continuation

核心区别只有一个:

是否占用线程。

如果你在 Dispatchers.Default 中大量使用 ReentrantLock,你很可能会:

  • 卡住线程池
  • 降低并发度
  • 产生性能抖动
  • 出现难以排查的调度问题

这不是“锁用得不对”。

是抽象层级错位。


四、协程时代真正应该用什么?

在现代 Kotlin 项目里,业务层应该优先使用:

  • Mutex
  • Channel
  • Flow
  • StateFlow
  • async / await
  • structured concurrency

比如:

image.png

更进一步,可以通过:

  • actor 模型
  • 单线程 dispatcher
  • 状态流建模

来避免显式锁。

这才是协程模型内部的并发方式。


五、什么时候可以用 AQS?

只有两种情况:

1️⃣ 写底层库

  • 自定义线程池
  • 自定义并发框架
  • JVM 基础组件

2️⃣ 强交互 Java 旧系统

  • 旧模块是 Java
  • 依赖 JUC API
  • 桥接遗留系统

否则:

业务层尽量不要出现 AQS 同步器。


六、显式使用 AQS,通常暴露什么问题?

在业务层看到:

  • ReentrantLock
  • CountDownLatch
  • synchronized

通常意味着:

  • 并发模型没统一
  • 协程和线程混用
  • 结构化并发没有真正落地
  • 分层设计不清晰

这不是“并发很复杂”。

这是:

抽象边界没有建立。


七、协程时代的并发分层

更健康的分层应该是:

业务层        → 协程原语
UseCase       → 协程编排
数据层        → 协程 + IO
底层基础库    → JVM 并发原语(可能包含 AQS)

而不是:

UseCase 直接操作线程锁
Repository 手动管理等待队列

AQS 是地基。

业务代码不应该住在地基里。


八、一句话原则

在协程项目中,不要用线程锁解决协程问题。

AQS 没过时。

但它不属于你的业务代码层。


九、真正成熟项目的标志

一个真正成熟的 Kotlin 项目应该是:

  • 全面协程化
  • 结构化并发
  • 无阻塞设计
  • 明确抽象边界
  • 业务层不感知线程模型

协程不是替代线程。

协程是对线程的抽象升级。

而架构设计真正的能力是:

把低层抽象封装在底层
不让它污染上层业务。

AQS 是基础设施。

业务代码应该站在抽象之上,而不是回到线程时代。