Kotlin协程面试题:suspend原理都说不清,协程你真会用?
1. synchronized vs volatile vs Atomic
核心回答
| 机制 | 保证的特性 | 底层实现 | 适用场景 |
|---|---|---|---|
| synchronized | 原子性 + 可见性 + 有序性 | Monitor 锁(monitorenter/monitorexit 字节码指令) | 复合操作的原子性保证 |
| volatile | 可见性 + 有序性(不含原子性) | 写屏障 + 读屏障(Happens-Before 语义) | 单次写入的状态标志位 |
| Atomic* | 原子性 + 可见性 + 有序性 | CAS 循环(CPU cmpxchg 指令) | 高并发下单一变量的原子操作 |
原理与代码
synchronized:Monitor 锁
synchronized 在字节码层面通过 monitorenter 和 monitorexit 两条指令实现。编译器为每个同步块生成两个 monitorexit(正常退出路径和异常退出路径),确保锁一定被释放。底层依赖 JVM 的 ObjectMonitor 机制,重量级锁状态下线程会进入阻塞态,涉及用户态到内核态的上下文切换。
kotlin
// Kotlin 中使用 synchronized 块(等价于 Java synchronized)
val lock = Any()
// 复合操作需要 synchronized 保证原子性
synchronized(lock) {
count++
}
// synchronized 修饰方法:编译器会设置 ACC_SYNCHRONIZED 标志位
// 线程进入方法前自动获取 monitor,退出时自动释放
// 双重检查锁单例:展示 volatile + synchronized 的组合使用
class Singleton private constructor() {
companion object {
// volatile 确保:1. 可见性 2. 禁止指令重排序(防止获取到半初始化的对象)
@Volatile
private var instance: Singleton? = null
fun getInstance(): Singleton {
return instance ?: synchronized(this) {
instance ?: Singleton().also { instance = it }
}
}
}
}
JDK 1.6 引入了偏向锁、轻量级锁、重量级锁的升级机制。偏向锁在单线程重复获取锁时消除同步开销;轻量级锁在多线程交替获取锁时使用 CAS 自旋避免内核态切换;重量级锁在竞争激烈时升级到依赖 OS Mutex 的 ObjectMonitor。
volatile:可见性与有序性
volatile 保证 Happens-Before 语义中的两条核心规则(JLS 17.4.5):
- 对 volatile 变量的写操作 Happens-Before 后续对该变量的读操作
- 解锁操作 Happens-Before 后续对同一锁的加锁操作
实现上,volatile 写操作后插入写屏障(强制刷新到主内存),读操作前插入读屏障(强制从主内存读取)。
kotlin
// Kotlin/JVM 中使用 Java 的 @Volatile 注解
@Volatile
var isReady = false
var flag = false // 普通变量:多线程下修改可能对其他线程不可见
// volatile 适合场景:状态标志位、单写多读
class ConnectionManager {
@Volatile
private var isConnected = false
fun setConnected(connected: Boolean) {
isConnected = connected // 写入后立即对其他线程可见
}
fun isConnected(): Boolean = isConnected
}
重要区分 :volatile 不保证 i++ 这样的复合操作的原子性。
kotlin
// 这个场景下 volatile 不够用
@Volatile var counter = 0
// 线程 A: read counter(0) → increment(=1) → write counter=1
// 线程 B: read counter(0) → increment(=1) → write counter=1
// 最终结果可能是 1 而非预期的 2
// 正确做法:使用 AtomicInteger
import java.util.concurrent.atomic.AtomicInteger
val counter = AtomicInteger(0)
fun increment() {
counter.incrementAndGet() // CAS 循环保证原子性
}
Atomic*:CAS 无锁原子操作
AtomicInteger 的 incrementAndGet() 底层调用 Unsafe.getAndAddInt(),该方法内部是一个自旋 CAS 循环:读取当前值 → 计算新值 → compareAndSet(对应 CPU 的 cmpxchg 原子指令)比较并交换,失败则重试直到成功。
java
import java.util.concurrent.atomic.AtomicInteger
import java.util.concurrent.atomic.AtomicStampedReference
// AtomicInteger 基本用法
val count = AtomicInteger(0)
count.incrementAndGet() // 原子递增,返回新值
count.getAndIncrement() // 原子递增,返回旧值
count.compareAndSet(5, 6) // CAS:只有当前值为 5 时才更新为 6
// ABA 问题:值从 A → B → A,CAS 无法感知中间修改
// 解决:使用 AtomicStampedReference,带版本号
val ref = AtomicStampedReference("A", 0)
val stamp = ref.getStamp()
ref.compareAndSet("A", "B", stamp, stamp + 1) // 只有版本号匹配时才更新
CAS 的三个主要缺陷:ABA 问题 (AtomicStampedReference 解决)、高竞争下自旋开销 (CPU 空转)、只能保证单一变量的原子性。
Android 实战场景
kotlin
// 场景 1:volatile 用于配置标志
class Config {
@Volatile
var debugMode = false
}
// 场景 2:synchronized 用于业务操作的原子性
class InventoryManager {
private val lock = Any()
private val stock = mutableMapOf<String, Int>()
fun reserve(itemId: String, quantity: Int): Boolean {
synchronized(lock) {
val current = stock[itemId] ?: 0
return if (current >= quantity) {
stock[itemId] = current - quantity
true
} else {
false
}
}
}
}
// 场景 3:AtomicInteger 用于高频计数器
class Metrics {
private val requestCount = AtomicInteger(0)
private val errorCount = AtomicInteger(0)
fun onRequest() { requestCount.incrementAndGet() }
fun onError() { errorCount.incrementAndGet() }
fun getErrorRate(): Double = errorCount.get().toDouble() / requestCount.get()
}
面试加分点
- 能说清楚 Happens-Before 规则(JLS 17.4.5)中与 synchronized/volatile 相关的两条:Monitor Lock Rule 和 Volatile Variable Rule
- 理解 synchronized 在 JDK 1.6 的锁升级优化机制,以及为什么 JDK 15 后偏向锁被废弃(JEP 374)
- 知道 CAS 底层依赖
cmpxchg指令,以及该指令在不同 CPU 架构(x86/ARM)上的差异 - 提到在协程中应优先使用
Mutex(kotlinx.coroutines.sync)而非synchronized,因为synchronized阻塞的是 OS 线程,与协程复用线程的设计相悖
2. 协程 suspend 原理
核心回答
suspend 函数不是"魔法阻塞",而是编译器通过 CPS(Continuation-Passing Style,续体传递风格) 将函数改造成携带额外 Continuation 参数的状态机 。挂起的本质是函数提前返回 COROUTINE_SUSPENDED 标记,交出线程控制权;恢复的本质是在合适线程调用 Continuation.resumeWith(result),从中断位置继续执行。
以下内容主要基于 Kotlin 语言规范(Asynchronous Programming with Coroutines,kotlinlang.org/spec)。
原理与代码
Continuation 接口
kotlin
// kotlin.coroutines.Continuation(Kotlin 语言规范定义)
public interface Continuation<in T> {
public val context: CoroutineContext
public fun resumeWith(result: Result<T>)
}
// kotlinx.coroutines 提供的便捷扩展
fun <T> Continuation<T>.resume(value: T) =
resumeWith(Result.success(value))
fun <T> Continuation<T>.resumeWithException(exception: Throwable) =
resumeWith(Result.failure(exception))
CPS 变换(Kotlin 语言规范原文)
根据 Kotlin 语言规范(Asynchronous Programming with Coroutines),suspend 函数的变换规则如下:
For a suspendable function with parameters p_1, p_2, ... and result type T, a new function is generated, with an additional parameter p_{N+1} of type
kotlin.coroutines.Continuation<T>and return type changed tokotlin.Any?.
返回类型变为 Any?,可以承载两种值:
- 实际结果类型 T:函数正常完成
COROUTINE_SUSPENDED标记(suspendCoroutineUninterceptedOrReturn 返回):函数被挂起
kotlin
// 原始代码
suspend fun getUser(id: String): User {
val user = fetchFromCache(id)
if (user != null) return user
return api.fetchUser(id)
}
状态机(Kotlin 语言规范原文示例)
根据 Kotlin 语言规范,编译器将包含 N 个挂起点的 lambda 编译为状态机,生成 N+M 个状态(N 个挂起点 + M 个非挂起的 return 语句):
kotlin
// 编译器生成的概念等价代码(Kotlin 语言规范中的状态机示例)
class GetUserStateMachine(
completion: Continuation<User>
) : Continuation<User> {
var label = 0 // 状态机当前位置
var result: User? = null // 局部变量提升为成员变量
override val context: CoroutineContext = completion.context
override fun resumeWith(result: Result<User>) {
when (label) {
0 -> {
// 执行 fetchFromCache
label = 1
val r = fetchFromCache(id, this)
if (r === COROUTINE_SUSPENDED) return
// 没有挂起,继续执行
this.result = r
resumeWith(Result.success(this.result!!))
}
1 -> {
// 从缓存恢复,检查结果
this.result = result.getOrNull()
label = 2
if (this.result != null) {
completion.resume(this.result!!)
} else {
// 调用 api.fetchUser,进入下一个挂起点
val r2 = api.fetchUser(id, this)
if (r2 === COROUTINE_SUSPENDED) return
completion.resume(r2 as User)
}
}
}
}
}
COROUTINE_SUSPENDED 标记
arduino
// Kotlin 协程规范的定义:
// 如果函数正常完成,返回结果值 T
// 如果函数被挂起,返回 COROUTINE_SUSPENDED 标记
// 由于 JVM 字节码不支持联合类型,返回类型使用 Any? 承载两种情况
// 挂起函数的返回类型变为 Any? 的原因:
// 必须能同时表示 实际结果(T) 和 COROUTINE_SUSPENDED 标记
// 这是 Kotlin 在 JVM 上的工程权衡
Android 实战场景
理解 suspend 原理有助于诊断协程行为:
kotlin
class UserRepository(private val api: UserApi) {
// 编译后:这个函数接受隐式的 Continuation 参数
// 挂起点:withContext 内部的网络调用
suspend fun loadUser(id: String): Result<User> {
val user = withContext(Dispatchers.IO) {
api.getUser(id)
}
return if (user != null) Result.success(user)
else Result.failure(Exception("Not found"))
}
}
// 常见错误:在 suspend 函数中调用阻塞代码
suspend fun bad() {
Thread.sleep(1000) // 阻塞线程,不释放(错误)
}
suspend fun good() {
delay(1000) // 挂起协程,线程可复用(正确)
}
面试加分点
- 能说清楚"挂起"和"阻塞"的本质区别:挂起是协程层面的暂停(函数 return),线程被释放;阻塞是线程层面的等待(线程不返回)
- 理解局部变量提升:挂起函数中的局部变量在挂起时需要保存,因此被提升为状态机类的成员变量
- 知道
suspendCoroutineUninterceptedOrReturn是编译器生成 suspend 函数体的核心 intrinsic - 理解 Kotlin 协程是 stackless coroutine(不保留调用栈),这与 Go goroutine(stackful)的重要区别
- 提到
kotlinx.coroutines.debug中的DCG格式日志可用于调试协程状态
3. CoroutineScope 与 Job
核心回答
结构化并发 是 Kotlin 协程的核心设计原则。每个协程必须属于一个 CoroutineScope,协程之间形成父子层级。Job 是这个层级关系的管理者:job.cancel() 取消该协程及其所有子协程(取消传播);子协程抛出非 CancellationException 时,父协程也会被取消(异常传播)。supervisorScope 和 SupervisorJob 可以打破这种传播,使子协程的失败互不影响。
原理与代码
Job 生命周期与父子关系
kotlin
import kotlinx.coroutines.*
// Job 的状态机:New → Active → Completing → Completed
// ↓
// Cancelling → Cancelled
// 取消传播:父 Job 取消 → 递归取消所有子 Job
fun cancellationPropagation() = runBlocking {
val parent = launch {
val child1 = launch { delay(500); println("Child 1 done") }
val child2 = launch { delay(300); println("Child 2 done") }
}
delay(100)
parent.cancel() // 取消父协程
parent.join()
// child1 和 child2 都会被取消
}
// 取消单个子协程,不影响父协程
fun cancelOneChild() = runBlocking {
val task1 = launch { /* ... */ }
val task2 = launch { /* ... */ }
task1.cancel() // 只取消 task1
}
// 异常传播:子协程非 CancellationException → 取消父协程 + 兄弟协程
fun exceptionPropagation() = runBlocking {
val job = launch {
launch {
try { delay(500); println("Task 1 done") }
finally { println("Task 1 cancelled") }
}
launch {
delay(100)
throw RuntimeException("Task 2 failed") // 异常传播到父协程
}
}
try { job.join() } catch (e: RuntimeException) { println("Caught: $e") }
}
SupervisorScope 与 SupervisorJob
kotlin
import kotlinx.coroutines.*
// SupervisorJob:子协程失败不影响父协程和兄弟协程
class ParallelFetcher {
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
// 场景:并行获取多个数据源,局部失败不影响整体
suspend fun fetchAll(): Map<String, Result<Data>> = supervisorScope {
val userDeferred = async { fetchUser() }
val postsDeferred = async { fetchPosts() }
val statsDeferred = async { fetchStats() }
mapOf(
"user" to runCatching { userDeferred.await() },
"posts" to runCatching { postsDeferred.await() },
"stats" to runCatching { statsDeferred.await() }
)
}
private suspend fun fetchUser(): Data = throw RuntimeException("user failed")
private suspend fun fetchPosts(): Data = Data("posts")
private suspend fun fetchStats(): Data = Data("stats")
data class Data(val value: String)
}
Android 实战场景
kotlin
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.*
class UserViewModel : ViewModel() {
// viewModelScope 使用 SupervisorJob,ViewModel 销毁时自动取消
// 单个子协程失败不会导致整个 ViewModel 不可用
fun loadUser(id: String) {
viewModelScope.launch {
_uiState.value = UiState.Loading
try {
val user = repository.loadUser(id)
_uiState.value = UiState.Success(user)
} catch (e: CancellationException) {
throw e // 必须重新抛出,否则协程无法正确取消
} catch (e: Exception) {
_uiState.value = UiState.Error(e.message ?: "Error")
}
}
}
// 并行加载,局部失败隔离:使用 supervisorScope
fun loadDashboard() {
viewModelScope.launch {
val result = withContext(Dispatchers.IO) {
supervisorScope {
val users = async { repository.getUsers() }
val posts = async { repository.getPosts() }
Dashboard(
users = users.await().getOrDefault(emptyList()),
posts = posts.await().getOrDefault(emptyList())
)
}
}
_uiState.value = UiState.Success(result)
}
}
// 注意:long-running 循环应检查 isActive
fun processItems(items: List<Item>) {
viewModelScope.launch {
items.forEach { item ->
if (!isActive) return@launch // 协程被取消时退出
processItem(item)
}
}
}
private val _uiState = MutableStateFlow<UiState>(UiState.Initial)
val uiState: StateFlow<UiState> = _uiState.asStateFlow()
private lateinit var repository: Repository
sealed class UiState {
data object Initial : UiState()
data object Loading : UiState()
data class Success(val data: Any) : UiState()
data class Error(val message: String) : UiState()
}
data class Dashboard(val users: List<Any>, val posts: List<Any>)
data class Item(val id: String)
}
面试加分点
- 理解
CoroutineScope和CoroutineContext的区别:Scope 是协程的"管理边界",Context 是协程的"配置集合" - 知道
viewModelScope使用SupervisorJob而非普通Job的原因 - 理解
CancellationException是协程取消的信号,被CoroutineExceptionHandler忽略,必须在 catch 块中重新抛出 - 提到
Job的六种状态:New、Active、Completing、Completed、Cancelling、Cancelled
4. Dispatchers
核心回答
Dispatchers.Default 和 Dispatchers.IO 共享底层线程池 (基于 CoroutineScheduler),通过 TaskContext(Boolean 值:Blocking/NonBlocking)区分任务类型。Dispatchers.Default 用于 CPU 密集型任务,Dispatchers.IO 用于阻塞型 IO 任务,并通过 limitedParallelism 限制并发数。Dispatchers.Main 在 Android 上基于 Handler 实现。
以下线程池实现细节基于 kotlinx.coroutines 源码(Dispatchers.kt、CoroutineScheduler.kt)。
原理与代码
调度器实现
arduino
// 线程池核心参数(kotlinx.coroutines 源码 - Tasks.kt)
// CORE_POOL_SIZE = max(2, AVAILABLE_PROCESSORS)
// MAX_POOL_SIZE = CoroutineScheduler.MAX_SUPPORTED_POOL_SIZE
// IDLE_WORKER_KEEP_ALIVE_NS = 60 秒
// Dispatchers.Default:使用共享线程池,适合 CPU 密集型任务
// Dispatchers.IO:与 Default 共享线程池,通过 TaskContext 区分阻塞任务
// IO 并发数限制:min(64, AVAILABLE_PROCESSORS)(可通过 kotlinx.coroutines.io.parallelism 配置)
// Android 上的 Dispatchers.Main(基于 kotlinx.coroutines.android)
// 内部使用 Handler(Looper.getMainLooper()),通过 post() 投递到主线程消息队列
// Dispatchers.Main.immediate:已处于主线程时直接执行,避免 post 延迟
withContext 线程切换
kotlin
// withContext 的执行流程:
// 1. 在原调度器上挂起协程
// 2. 在目标调度器上执行 block
// 3. 恢复原协程(在目标线程或另一线程,取决于实现)
// 重要特性:Dispatchers.Default 和 IO 共享线程池
// 在 Default 上调用 withContext(IO) 通常不需要实际切换线程
// 调度器会尽量在同一个物理线程上继续执行
// 正确选择调度器
import kotlinx.coroutines.*
class Repository {
suspend fun fetchUser(): User {
return withContext(Dispatchers.IO) { // IO 密集:网络/文件
api.getUser()
}
}
suspend fun parseData(data: String): Parsed {
return withContext(Dispatchers.Default) { // CPU 密集:JSON 解析、加密
parseJson(data)
}
}
}
Android 实战场景
kotlin
import kotlinx.coroutines.*
import androidx.lifecycle.ViewModel
class SearchViewModel : ViewModel() {
fun search(query: String) {
viewModelScope.launch {
// 自动在 Dispatchers.Main 执行
val result = withContext(Dispatchers.IO) {
repository.search(query)
}
_results.value = result
}
viewModelScope.launch(Dispatchers.Default) {
// CPU 密集处理直接在 Default 调度器执行
val processed = processSearchResults(query)
_processedResults.value = processed
}
}
// 并行 IO 任务:async + awaitAll
fun loadAll() {
viewModelScope.launch {
val (users, posts) = withContext(Dispatchers.IO) {
val usersDeferred = async { repository.getUsers() }
val postsDeferred = async { repository.getPosts() }
usersDeferred.await() to postsDeferred.await()
}
_combinedData.value = CombinedData(users, posts)
}
}
private val _results = MutableStateFlow<List<String>>(emptyList())
private val _processedResults = MutableStateFlow<List<String>>(emptyList())
private val _combinedData = MutableStateFlow<CombinedData?>(null)
private lateinit var repository: Repository
}
data class CombinedData(val users: List<Any>, val posts: List<Any>)
面试加分点
- 了解
CoroutineScheduler支持工作窃取(work-stealing):当 worker 的本地队列为空时,从其他 worker 队列窃取任务 - 理解
Dispatchers.IO.limitedParallelism创建的是调度器的"视图",不创建新线程池,限制的是并发任务数 - 知道
Dispatchers.Main.immediate与Dispatchers.Main的区别:前者避免不必要的 post - 提到
Dispatchers.Unconfined不推荐日常使用:行为不可预测,可能导致意外的线程亲和性
5. 协程异常处理
核心回答
协程异常处理遵循结构化并发的层级规则:子协程的非 CancellationException 异常会传播到父协程,取消父协程及其所有兄弟协程。CoroutineExceptionHandler 仅对根协程 (直接位于 GlobalScope 或带 SupervisorJob 的 scope 的协程)生效,对普通 coroutineScope {} 的子协程无效。
原理与代码
异常传播机制
scss
import kotlinx.coroutines.*
// 默认行为:子协程异常 → 取消父协程和所有兄弟协程
fun defaultBehavior() = runBlocking {
launch {
try { delay(500); println("Task 1") }
finally { println("Task 1 cancelled") }
}
launch {
delay(100)
throw RuntimeException("Task 2 failed") // 取消其他所有子协程
}
delay(200) // 可能抛出异常
}
// CoroutineExceptionHandler 仅对根协程生效
fun handlerEffectiveness() = runBlocking {
val handler = CoroutineExceptionHandler { _, exception ->
println("Handler caught: $exception")
}
// 有效:根协程直接位于 GlobalScope
GlobalScope.launch(handler) {
throw RuntimeException("Caught")
}.join()
// 有效:handler 在根协程上(SupervisorJob 的直接子协程)
val scope = CoroutineScope(SupervisorJob())
scope.launch(handler) {
// handler 会捕获此处及子协程传播上来的异常
throw RuntimeException("Caught by handler")
}
// 无效:handler 在非根协程上(子协程的子协程)
val scope2 = CoroutineScope(SupervisorJob())
scope2.launch {
// handler 写在内层 launch 上无效,异常仍传播到根协程处理
launch(handler) { throw RuntimeException("Not caught by this handler") }
}
delay(100)
scope.cancel()
scope2.cancel()
}
try-catch 的陷阱
kotlin
import kotlinx.coroutines.*
// 陷阱 1:launch 返回前就抛出的异常------无法在外部捕获
fun trap1() = runBlocking {
try {
launch {
throw RuntimeException("In launch") // 异步抛出,不在 launch 调用处
}
delay(100)
} catch (e: Exception) {
println("Never here") // 不会执行
}
}
// 陷阱 2:吞掉 CancellationException
fun trap2() = runBlocking {
val job = launch {
try {
delay(1000)
} catch (e: Exception) {
// 错误:吞掉了 CancellationException
println("Caught: $e")
}
println("After catch") // 协程不会正确取消
}
delay(100)
job.cancelAndJoin()
}
// 正确做法
fun correct() = runBlocking {
val job = launch {
try {
riskyOperation()
} catch (e: CancellationException) {
throw e // 必须重新抛出
} catch (e: IOException) {
println("Network error: $e")
}
}
}
Android 实战场景
kotlin
import kotlinx.coroutines.*
import androidx.lifecycle.ViewModel
class RobustViewModel : ViewModel() {
// 最佳实践:supervisorScope 隔离并行任务
fun loadDashboard() {
viewModelScope.launch {
val result = withContext(Dispatchers.IO) {
supervisorScope {
val users = async { repo.getUsers() }
val posts = async { repo.getPosts() }
Dashboard(
users = users.await().getOrDefault(emptyList()),
posts = posts.await().getOrDefault(emptyList())
)
}
}
_uiState.value = UiState.Success(result)
}
}
// runCatching 简洁错误处理
fun loadSafely() {
viewModelScope.launch {
val result = runCatching {
withContext(Dispatchers.IO) { repo.fetchAll() }
}
_uiState.value = result.fold(
onSuccess = { UiState.Success(it) },
onFailure = { UiState.Error(it.message ?: "Error") }
)
}
}
private val _uiState = MutableStateFlow<UiState>(UiState.Initial)
private lateinit var repo: Repository
sealed class UiState {
data object Initial : UiState()
data class Success(val data: Any) : UiState()
data class Error(val message: String) : UiState()
}
data class Dashboard(val users: List<Any>, val posts: List<Any>)
}
面试加分点
- 理解
CancellationException在协程异常体系中的特殊地位:它是取消信号,被CoroutineExceptionHandler忽略 - 知道异常聚合:多个子协程同时失败时,只有第一个异常被处理,其余附加为 suppressed exception
- 理解为什么
CoroutineExceptionHandler对coroutineScope {}的子协程无效------coroutineScope {}本身就是异常传播机制的实现者
6. Flow vs StateFlow vs SharedFlow vs LiveData
核心回答
| 类型 | 热/冷 | 多播 | 缓存 | 初始值 | 典型场景 |
|---|---|---|---|---|---|
| Flow | 冷 | 单播 | 无 | 无 | 一次性数据流、网络请求 |
| StateFlow | 热 | 多播 | 最新值 | 必须有 | UI 状态管理 |
| SharedFlow | 热 | 多播 | 可配置 | 无 | 事件总线、一次性事件 |
| LiveData | 热 | 多播 | 最新值 | 无(可观察 null) | Android 旧架构迁移前 |
原理与代码
Flow(冷流)
scss
import kotlinx.coroutines.flow.*
// Flow 是冷数据流:每次 collect 重新执行发射代码
fun fetchUserList(): Flow<List<User>> = flow {
val users = api.getUserList() // 每次 collect 都会调用
emit(users)
}.flowOn(Dispatchers.IO)
// Flow 的背压处理
suspend fun backpressure() {
flow {
repeat(1000) { emit(it) }
}.buffer(50) // 增加缓冲区
.collect { value ->
delay(10)
println(value)
}
}
StateFlow(状态容器)
kotlin
import kotlinx.coroutines.flow.*
// StateFlow = 热流 + 最新值缓存 + 必须有初始值
class UiStateStore {
private val _state = MutableStateFlow(State.Loading)
val state: StateFlow<State> = _state.asStateFlow()
fun load() {
CoroutineScope(Dispatchers.IO).launch {
try {
val data = repository.fetch()
_state.value = State.Success(data)
} catch (e: Exception) {
_state.value = State.Error(e.message ?: "Error")
}
}
}
sealed class State {
data object Loading : State()
data class Success(val data: String) : State()
data class Error(val message: String) : State()
}
}
SharedFlow(事件总线)
kotlin
import kotlinx.coroutines.flow.*
// SharedFlow = 热流 + 可配置 replay + 无初始值
// replay = 0:新订阅者不接收历史事件(一次性事件)
// replay = 1:保留最新一个事件
class EventBus {
private val _events = MutableSharedFlow<Event>(
replay = 0,
extraBufferCapacity = 64,
onBufferOverflow = BufferOverflow.DROP_OLDEST
)
val events: SharedFlow<Event> = _events.asSharedFlow()
fun emit(event: Event) {
_events.tryEmit(event)
}
}
// SharedFlow 用于一次性事件(替代 LiveData "single event" 模式)
// 注意:需要在 View 层用 first() 确保一次性消费
与 LiveData 的关键区别
kotlin
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import kotlinx.coroutines.flow.*
// LiveData 与 StateFlow 的关键差异
// 1. StateFlow 必须有初始值(LiveData 不需要)
// 2. StateFlow 不允许 null(LiveData 可观察 null)
// 3. LiveData 自动跟随 Android 生命周期(isAtLeast STARTED)
// StateFlow 需要 repeatOnLifecycle 才能达到相同效果
class Fragment : androidx.fragment.app.Fragment() {
override fun onViewCreated(view: android.view.View, savedInstanceState: android.os.Bundle?) {
super.onViewCreated(view, savedInstanceState)
// LiveData:自动生命周期管理
viewModel.liveData.observe(viewLifecycleOwner) { value ->
// 视图 DESTROYED 时自动取消订阅
}
// StateFlow:需要 repeatOnLifecycle
viewLifecycleOwner.lifecycleScope.launch {
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.stateFlow.collect { value ->
// STARTED 时收集,STOPPED 时暂停
}
}
}
// Jetpack Compose 中不需要 repeatOnLifecycle
// collectAsState() 内部自动处理
}
}
Android 实战场景
kotlin
import kotlinx.coroutines.flow.*
import androidx.lifecycle.ViewModel
class ArticleViewModel : ViewModel() {
private val _uiState = MutableStateFlow<UiState>(UiState.Initial)
val uiState: StateFlow<UiState> = _uiState.asStateFlow()
private val _events = MutableSharedFlow<UiEvent>(replay = 0)
val events: SharedFlow<UiEvent> = _events.asSharedFlow()
fun loadArticle(id: String) {
viewModelScope.launch {
_uiState.value = UiState.Loading
val result = runCatching {
withContext(Dispatchers.IO) { repo.getArticle(id) }
}
_uiState.value = result.fold(
onSuccess = { UiState.Success(it) },
onFailure = { UiState.Error(it.message ?: "Error") }
)
}
}
sealed class UiState {
data object Initial : UiState()
data object Loading : UiState()
data class Success(val article: Article) : UiState()
data class Error(val message: String) : UiState()
}
sealed class UiEvent {
data class ShowSnackbar(val message: String) : UiEvent()
data class Navigate(val route: String) : UiEvent()
}
data class Article(val title: String)
}
面试加分点
- 理解
SharedFlow(replay = 0)是实现"一次性事件"的标准方案,新订阅者不接收历史事件 - 知道
stateIn()和shareIn()是将冷 Flow 转换为热 Flow 的标准操作符 - 理解在 Jetpack Compose 中
collectAsState()内部已处理 lifecycle-aware 逻辑,无需额外repeatOnLifecycle
7. Channel
核心回答
Channel 是协程间点对点通信的"管道",与 Flow 的核心区别是:Channel 是热端到端管道 (强调发送和接收的同步握手),Flow 是声明式数据流(强调转换管道)。Channel 天然处理背压(缓冲区满则发送端挂起),适合协程间通信、工作分发;Flow 适合数据流处理、多订阅广播。
原理与代码
Channel 的四种类型
kotlin
import kotlinx.coroutines.*
import kotlinx.coroutines.channels.*
// 1. Rendezvous Channel(默认,容量 0)
// 发送和接收必须"碰头"------发送端挂起直到接收端准备好
val rendezvous = Channel<String>(Channel.RENDEZVOUS)
// 2. Buffered Channel(固定容量)
// 缓冲区满后,发送端挂起;缓冲区空时,接收端挂起
val buffered = Channel<Int>(capacity = 64)
// 3. Conflated Channel(容量 1,新值覆盖旧值)
// 发送端不等待接收端,新值直接覆盖旧值
val conflated = Channel<Int>(Channel.CONFLATED)
// 4. Unlimited Channel(无限制)
// 发送端永不挂起,内部使用 LinkedList(有 OOM 风险)
val unlimited = Channel<Int>(Channel.UNLIMITED)
Channel vs Flow 的核心区别
scss
import kotlinx.coroutines.*
import kotlinx.coroutines.channels.*
import kotlinx.coroutines.flow.*
// 核心区别 1:发送 vs 接收控制权
fun sendVsReceive() = runBlocking {
// Channel:发送端控制推送(热)
val channel = Channel<Int>()
launch { channel.send(1); channel.send(2); channel.close() }
// Flow:接收端控制拉取(冷)
val flow = flow { emit(1); emit(2) }
}
// 核心区别 2:多订阅行为
fun multiSubscriber() = runBlocking {
// Flow 多订阅:每个订阅者独立执行(重新发射)
val flow = flow { emit(1) }
launch { flow.collect { println("A: $it") } }
launch { flow.collect { println("B: $it") } } // 独立重新执行
delay(100)
// Channel 多订阅(fan-out):值只发送一次,多个接收者竞争消费
val channel = Channel<Int>()
launch { channel.send(1); channel.close() }
// 多个接收者:每个值只能被一个接收者消费
}
// Channel fan-out(工作分发)
fun fanOut() = runBlocking {
val tasks = Channel<Int>(Channel.BUFFERED)
launch { repeat(10) { tasks.send(it) }; tasks.close() }
repeat(3) { workerId ->
launch {
for (task in tasks) {
println("Worker $workerId processed $task")
}
}
}
delay(500)
}
面试加分点
- 理解
onUndeliveredElement机制:Channel 关闭或取消时处理未交付元素,可用于资源释放(如关闭文件句柄) - 知道
Channel.BUFFERED默认容量为 64(kotlinx.coroutines.channels.DEFAULT_BUFFER_PROPERTY_KEY的值) - 理解
BroadcastChannel已被弃用,SharedFlow是其标准替代方案 - 理解 Channel 的背压通过挂起自动处理,而 Flow 需要使用
buffer等操作符
8. Android 异步方案演进
核心回答
Android 异步方案经历了四次主要演进:Handler (线程间通信基础)→ AsyncTask (已废弃,生命周期绑定不当)→ RxJava (强大但复杂,学习曲线陡)→ Kotlin 协程 (结构化并发、轻量级、编译期检查、与语言深度集成)。协程最终胜出的核心原因:结构化并发保证生命周期安全 + 线程复用实现轻量级并发 + Flow/Channel 覆盖完整异步场景。
原理与代码
Handler:线程间通信基石
kotlin
import android.os.Handler
import android.os.Looper
// Handler 将 Runnable post 到 MessageQueue,由 Looper 驱动执行
class HandlerExample(private val handler: Handler) {
fun fetchData() {
Thread {
val data = networkCall()
handler.post { updateUI(data) } // 切换到主线程
}.start()
}
private fun networkCall(): String = "data"
private fun updateUI(data: String) { /* UI 操作 */ }
}
AsyncTask:已废弃
AsyncTask(API 30+ 废弃)的核心问题:与 Activity 生命周期绑定不当。配置变更(如屏幕旋转)后,AsyncTask 可能持有旧 Activity 引用,导致内存泄漏或空指针。
RxJava:强大但复杂
kotlin
import io.reactivex.rxjava3.core.*
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.schedulers.Schedulers
// RxJava 的问题:
// 1. Backpressure 配置复杂
// 2. Disposable 需要手动释放(内存泄漏风险)
// 3. 异常栈链长,调试困难
// 4. 与 Kotlin 集成不完整
fun search(query: String): Disposable {
return api.search(query)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.debounce(300, TimeUnit.MILLISECONDS)
.subscribe(
{ result -> /* 处理 */ },
{ error -> /* 处理 */ }
)
}
Kotlin 协程:最终方案
kotlin
import kotlinx.coroutines.*
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.flow.*
class CoroutineViewModel : ViewModel() {
// viewModelScope 自动绑定 ViewModel 生命周期
// Activity 销毁 → Scope 取消 → 所有协程自动取消(无内存泄漏)
fun fetchData() {
viewModelScope.launch {
try {
val result = withContext(Dispatchers.IO) {
repository.getData()
}
_uiState.value = UiState.Success(result)
} catch (e: CancellationException) {
throw e
} catch (e: Exception) {
_uiState.value = UiState.Error(e.message ?: "Error")
}
}
}
fun parallelLoad() {
viewModelScope.launch {
val (user, posts) = withContext(Dispatchers.IO) {
val userDeferred = async { repository.getUser() }
val postsDeferred = async { repository.getPosts() }
userDeferred.await() to postsDeferred.await()
}
_uiState.value = UiState.Success(Combined(user, posts))
}
}
private val _uiState = MutableStateFlow<UiState>(UiState.Initial)
val uiState: StateFlow<UiState> = _uiState.asStateFlow()
sealed class UiState {
data object Initial : UiState()
data class Success(val data: Any) : UiState()
data class Error(val message: String) : UiState()
}
data class Combined(val user: Any, val posts: Any)
}
// 与 Compose 集成
@Composable
fun Screen(viewModel: CoroutineViewModel = viewModel()) {
val uiState by viewModel.uiState.collectAsState()
when (val state = uiState) {
is CoroutineViewModel.UiState.Success -> Content(data = state.data)
is CoroutineViewModel.UiState.Error -> Error(message = state.message)
CoroutineViewModel.UiState.Initial -> {}
}
}
面试加分点
- 能说清楚协程与线程的一对多关系:多个协程可以共享一个线程,实现高效并发
- 理解协程的取消是协作式 的:需要在挂起函数中检查
isActive;长时间运行的循环应使用yield()或ensureActive() - 提到 Jetpack 组件(Room、DataStore、WorkManager 等)对协程的一等支持,都提供了 suspend 版本和 Flow 返回值
- 理解在 Java-only 项目中,
CompletableFuture+ExecutorService是次优选择
总结
这 8 道题目覆盖了 Kotlin 协程与并发的核心知识体系。面试中理解底层原理比记忆 API 重要得多:
- synchronized/volatile/Atomic:理解 JMM 的 Happens-Before 语义和 CAS 原理,才能在正确场景下选择正确工具
- suspend:理解 CPS 变换和状态机,才能理解"挂起不阻塞"的本质------挂起是函数 return,线程被释放复用
- 结构化并发:理解 Job 树和异常传播机制,才能写出生命周期安全的并发代码
- Flow 家族:理解冷热流、replay 语义和背压处理的差异,才能选择正确的响应式方案
- 演进历史:理解每一代方案的缺陷和协程的改进,才能在架构设计中做出正确决策
理解原理是跨越"会用"和"用好"之间鸿沟的唯一路径。