Kotlin协程面试题:suspend原理都说不清,协程你真会用?

Kotlin协程面试题:suspend原理都说不清,协程你真会用?

1. synchronized vs volatile vs Atomic

核心回答

机制 保证的特性 底层实现 适用场景
synchronized 原子性 + 可见性 + 有序性 Monitor 锁(monitorenter/monitorexit 字节码指令) 复合操作的原子性保证
volatile 可见性 + 有序性(不含原子性) 写屏障 + 读屏障(Happens-Before 语义) 单次写入的状态标志位
Atomic* 原子性 + 可见性 + 有序性 CAS 循环(CPU cmpxchg 指令) 高并发下单一变量的原子操作

原理与代码

synchronized:Monitor 锁

synchronized 在字节码层面通过 monitorentermonitorexit 两条指令实现。编译器为每个同步块生成两个 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 to kotlin.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 时,父协程也会被取消(异常传播)。supervisorScopeSupervisorJob 可以打破这种传播,使子协程的失败互不影响。

原理与代码

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

面试加分点

  • 理解 CoroutineScopeCoroutineContext 的区别:Scope 是协程的"管理边界",Context 是协程的"配置集合"
  • 知道 viewModelScope 使用 SupervisorJob 而非普通 Job 的原因
  • 理解 CancellationException 是协程取消的信号,被 CoroutineExceptionHandler 忽略,必须在 catch 块中重新抛出
  • 提到 Job 的六种状态:New、Active、Completing、Completed、Cancelling、Cancelled

4. Dispatchers

核心回答

Dispatchers.DefaultDispatchers.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.immediateDispatchers.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
  • 理解为什么 CoroutineExceptionHandlercoroutineScope {} 的子协程无效------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 语义和背压处理的差异,才能选择正确的响应式方案
  • 演进历史:理解每一代方案的缺陷和协程的改进,才能在架构设计中做出正确决策

理解原理是跨越"会用"和"用好"之间鸿沟的唯一路径。

相关推荐
Kapaseker12 小时前
Android 官方开始拥抱 WebView
android
神奇小汤圆12 小时前
一个程序员眼中的 AI 核心概念,讲透 LLM 、Agent 、MCP 、Skill 、RAG...
面试
ujainu小13 小时前
CANN hixl:大模型 PD 分离场景的零拷贝通信库
android·java·缓存
张元清13 小时前
React 指针 Hook:Hover、长按、双击、刮擦和点击外部,告别那些经典 bug
前端·javascript·面试
专注VB编程开发20年13 小时前
b4a用VB语言开发安卓APP-图片缩放库ZoomImageView讲解-双指缩放 + 单指拖动核心源码
android·java·前端
恋猫de小郭14 小时前
Dart 大更新,新增语法糖和各种能力,真的难得了
android·前端·flutter
怕浪猫14 小时前
Electron 开发实战(二):核心概念深度详解 | 进程通信+窗口高级+生命周期全掌握
前端·javascript·面试
EQ-雪梨蛋花汤14 小时前
【Sceneform-EQR】让Android 原生 3D开发更容易
android·3d
三少爷的鞋15 小时前
Android 架构指南之Data 层不要再暴露 start/stop 了:用 Flow 接管生命周期
android