2026年面试2

31 阅读11分钟

一、炫稷网络

1、子协程怎么运行到外部协程上?

最后,我会强调“运行到外部协程”是一种简化说法,实际是挂起机制和结构化并发的作用。考虑到用户的技术背景,准备提供一个更高级的实战技巧,比如用supervisorScope处理子协程异常,这可能在面试或实际开发中会用到。

这是一个关于 Kotlin 协程结构化并发核心机制的好问题。“子协程运行到外部协程上”这个说法,更准确的理解是:子协程在由外部协程(父协程)所建立的协程作用域和上下文中启动、运行,并最终将其结果、异常或取消状态汇报给父协程,整个过程是挂起与恢复的协作过程。

2、核心机制:结构化并发

这是协程管理并发的范式。当你使用 CoroutineScope.launchasync 在一个协程内部启动新协程时,就创建了一个父子关系

  • 子协程继承父协程的 CoroutineContext(尤其是 Job 和调度器 Dispatcher)。
  • 父协程会等待所有子协程完成后,自己才真正完成。
  • 取消父协程会同时取消所有子协程(可选的异常处理机制如 SupervisorJob 除外)。

2、如何“运行到”父协程上:挂起与恢复

这是理解的关键。协程是“协作式”的。

  • 挂起点:当子协程内部调用了一个挂起函数(如 delay, withContext, suspend fun)时,它并不会阻塞线程,而是将自己挂起,释放当前线程。
  • 线程利用:释放出的线程(例如 Dispatchers.IO 中的一个线程)就可以去执行其他可运行的协程任务。此时,外部(父)协程可能正挂起在等待子协程完成的地方(如 await()join())。
  • 恢复:当子协程的挂起操作完成(如 delay 时间到),它会被重新调度到可用的线程上继续执行,最终完成并通知父协程。

“运行到外部协程上”的常见实现方式:

// 方式1: 在父协程作用域内直接启动子协程(最常用)
fun example1() = runBlocking { // 外部协程,作用域创建者
    launch { // 子协程,继承外部协程的上下文
        delay(1000L)
        println("子协程完成") // 这个代码块运行在父协程建立的作用域内
    }
    println("外部协程继续,并等待子协程")
}

// 方式2: 使用 withContext “切换”到子协程上下文并挂起外部协程
suspend fun example2() = withContext(Dispatchers.Main) { // 外部协程在主线程
    val result = withContext(Dispatchers.IO) { // 切换到IO线程执行子协程块
        // 这个代码块运行在IO线程上,但外部协程挂起并等待
        performNetworkRequest() // 假设这是一个挂起的网络请求
    }
    // 执行到此,已自动切回主线程,result可用
    updateUI(result) // 外部协程恢复执行
}

3、关键组件的作用

  • CoroutineScope:定义了协程的生命周期范围。所有协程构建器(如 launch, async)都是它的扩展函数。Activity 的 lifecycleScope 或 ViewModel 的 viewModelScope 都是典型的、绑定生命周期的外部作用域。
  • 调度器:决定了协程在哪个或哪些线程上执行。Dispatchers.Main, Dispatchers.IO, Dispatchers.Default 等。子协程默认继承父协程的调度器,但可以通过 withContext 临时切换。

4、总结:

所以,“子协程怎么运行到外部协程上”可以理解为:

  1. 通过作用域嵌套:在外部协程的作用域内启动子协程,建立父子关系。
  2. 通过上下文继承:子协程默认共享父协程的上下文(调度器、Job等)。
  3. 通过挂起函数协作:子协程通过挂起函数让出线程,外部协程在适当的时候(如调用 join)挂起并等待,最终由协程框架调度恢复。

5、recycleView的刷新方式有多少种?

  • 整体刷新:notifyDataSetChanged()
  • 精确局部刷新:notifyItemXxx() 系列方法

原理RecyclerView 会根据你提供的精确位置(索引),仅对受影响的 ItemView 执行添加、移除、移动或更新动画,其他未变化的项不会重新绑定,效率极高。

  • 常用方法
// 插入
notifyItemInserted(position)
notifyItemRangeInserted(startPosition, itemCount)

// 删除
notifyItemRemoved(position)
notifyItemRangeRemoved(startPosition, itemCount)

// 更新(内容变化但位置不变)
notifyItemChanged(position)
notifyItemRangeChanged(startPosition, itemCount)

// 移动
notifyItemMoved(fromPosition, toPosition)
  • 仅小部分数据字段变化 notifyItemChanged(position, payload)

选择策略与最佳实践

场景推荐方式原因
单条数据内容变化(如点赞数更新)notifyItemChanged(position)notifyItemChanged(position, payload)最精确,可配合payload局部更新视图,避免重绑整个Item。
明确位置的数据插入/删除(如滑动删除)notifyItemInserted(position) / notifyItemRemoved(position)直接高效,有动画效果。
数据集合整体变化且结构复杂(如搜索过滤、重新排序)DiffUtilListAdapter自动计算差异,避免手动管理索引的复杂和出错。
仅小部分数据字段变化notifyItemChanged(position, payload)onBindViewHolder 中通过 payload 参数进行优化更新。

6、payload刷新原理:

当被问到如何只刷新一个控件时,核心答案是 notifyItemChanged(position, payload) 配合重载的 onBindViewHolder。你需要解释清楚:

  1. payload 的作用:它是一个“变化描述符”。
  2. 处理流程:在 onBindViewHolder(..., payloads) 中判断 payloads 列表,并根据不同的 payload 对象执行不同的控件更新。
  3. 性能优势:避免了完整的重新绑定,是实现高性能列表的关键优化手段之一。

7、java中线程的状态有多少种?

在Java中,线程的状态在 java.lang.Thread.State 枚举中明确定义,共有 6种。这是一个非常基础且重要的面试点。

6种线程状态详解

  1. NEW (新建)

    • 描述:线程对象已被创建(通过 new Thread()),但尚未调用 start() 方法。
    • 状态转换:调用 start() 后进入 RUNNABLE 状态。
  2. RUNNABLE (可运行)

    • 描述:这是最容易被误解的状态。它表示线程正在运行或已准备好运行,正在等待CPU时间片。这包含了操作系统线程状态中的 RunningReady
    • 关键点:线程是否正在CPU上执行,对于Java虚拟机(JVM)层面来说是不可见的。因此,只要线程已启动且未被阻塞,就处于此状态。
  3. BLOCKED (阻塞)

    • 描述:线程正在等待获取一个监视器锁(monitor lock) ,以便进入一个由 synchronized 关键字保护的同步块或方法。
    • 触发场景:另一个线程正持有该锁。只有 synchronized 会导致此状态Lock 接口的锁不会。
    • 状态转换:当持有锁的线程释放锁,且该线程竞争到锁时,会回到 RUNNABLE 状态。
  4. WAITING (无限期等待)

    • 描述:线程进入等待状态,需要被其他线程显式地唤醒。进入此状态会释放CPU时间片和持有的锁。

    • 触发方法

      • Object.wait() (不指定超时时间)
      • Thread.join() (不指定超时时间)
      • LockSupport.park()
    • 状态转换:需要被其他线程唤醒(如 Object.notify()/notifyAll(),或目标线程终止)。

  5. TIMED_WAITING (限期等待)

    • 描述:线程进入等待状态,但在指定的时间后会自动唤醒,或在此时间内被其他线程唤醒。

    • 触发方法

      • Thread.sleep(long millis)
      • Object.wait(long timeout)
      • Thread.join(long millis)
      • LockSupport.parkNanos()
      • LockSupport.parkUntil()
    • 状态转换:超时时间到达或被其他线程唤醒。

  6. TERMINATED (终止)

    • 描述:线程已经执行完毕(run() 方法正常结束)或因异常而退出。处于此状态的线程不能被再次启动。

8、面试核心要点与常见误区

  1. RUNNABLE 状态的本质:这是最重要的区分点。在Java层面,只要线程调用了 start() 且没有被synchronized阻塞、没有调用会进入WAITING/TIMED_WAITING的方法,它就一直是 RUNNABLE 状态,不管CPU是否正在执行它。

  2. BLOCKEDWAITING/TIMED_WAITING 的区别

    • BLOCKED 是因为竞争不到 synchronized而被动阻塞。这是一个“等锁”的状态。
    • WAITING/TIMED_WAITING 通常是线程主动调用了某个方法(如 wait(), sleep())而进入的。这是一个“等条件/等时间”的状态。
  3. 状态转换的不可逆性:线程状态是单向演进的。例如,不能从 TERMINATED 回到 NEWRUNNABLE(再次调用 start() 会抛出 IllegalThreadStateException)。

  4. 工具观察:在调试或排查问题时,可以通过 jstack 命令、Java VisualVM 或 IDE 的调试工具查看所有线程的当前状态。

9、协程的启动模式?

协程的启动模式通过 start 参数在 launchasync 构建器中指定,主要有以下4种:

启动模式参数核心行为典型使用场景
DEFAULTCoroutineStart.DEFAULT立即调度,在调度器决定的线程上尽快执行绝大多数场景下的默认选择。
LAZYCoroutineStart.LAZY延迟启动,仅当需要结果(await/join)或手动 start() 时才执行。任务定义但不立即执行,或构建任务依赖链。
ATOMICCoroutineStart.ATOMIC立即调度,但在开始执行前无法被取消(即使已调用 cancel)。极少使用,需要保证协程在启动前不因取消而中断的极端场景。
UNDISPATCHEDCoroutineStart.UNDISPATCHED立即在当前线程开始执行,直到遇到第一个挂起点,之后由调度器决定。优化:在当前线程立即执行初始代码,避免不必要的线程切换开销。

下面我们来详细解析最常用的两种和两种特殊情况。

10、DEFAULT(默认模式)

协程创建后,会根据其上下文中的 CoroutineDispatcher 立即被调度(安排到相应的线程池或线程)。注意,“立即调度”不等于“立即执行”,它只是进入了就绪队列,由调度器决定何时真正开始执行。

fun main() = runBlocking {
    println("主线程开始: ${Thread.currentThread().name}")

    val job = launch(start = CoroutineStart.DEFAULT) { // start = DEFAULT 可省略
        println("协程执行: ${Thread.currentThread().name}")
    }

    delay(100) // 给一点时间,确保协程被调度执行
    job.join()
}
// 输出示例:
// 主线程开始: main
// 协程执行: main (如果使用Dispatchers.Default,可能输出类似 `DefaultDispatcher-worker-1`)

11、LAZY(懒惰模式)

协程被创建后,不会立即被调度执行。它只在以下两种情况下才会启动:

  1. 手动调用其 Jobstart() 方法。
  2. 首次调用其 Jobjoin() 方法,或对于 async 创建的 Deferred,调用 await() 方法。
fun main() = runBlocking {
    println("程序开始")

    val lazyJob = launch(start = CoroutineStart.LAZY) {
        println("Lazy 协程执行了!")
    }

    println("Lazy 协程已创建,但未执行")
    delay(1000) // 即使等待,它也不会执行
    println("准备手动启动...")
    
    lazyJob.start() // 关键:手动触发执行
    // lazyJob.join() // 调用 join() 同样会触发执行
    
    lazyJob.join() // 等待它完成
}
// 输出:
// 程序开始
// Lazy 协程已创建,但未执行
// 准备手动启动...
// Lazy 协程执行了!

LAZY 模式与标准库 lazy 函数的区别

  • CoroutineStart.LAZY 控制协程执行的时机
  • Kotlin 标准库的 lazy { ... } 函数是用于值的延迟初始化,只计算一次并缓存结果。两者概念不同,切勿混淆。

12、ATOMIC(原子模式)

协程会像 DEFAULT 一样被立即调度,但在开始执行第一行代码之前,它将不受取消操作的影响。这是一种非常低级且极少使用的模式,通常用于协程内部实现或极特殊的并发控制场景,日常开发应避免使用,因为它破坏了结构化并发的取消机制。

13、UNDISPATCHED(未分发模式)

协程会立即在当前调用者的线程上开始执行,直到遇到第一个真正的挂起点(如 delaywithContextsuspend fun 调用)。在挂起点之后,协程的恢复将由它上下文中所配置的调度器决定。

fun main() = runBlocking { println("主线程: ${Thread.currentThread().name}")

launch(start = CoroutineStart.UNDISPATCHED) {
    println("协程第一段代码: ${Thread.currentThread().name}") // 在主线程执行!
    delay(100) // ← 这是一个挂起点
    println("协程恢复后: ${Thread.currentThread().name}") // 在默认调度器线程执行
}

println("主线程继续...")

} // 输出示例: // 主线程: main // 协程第一段代码: main <-- 立即在当前线程执行 // 主线程继续... <-- 注意打印顺序,说明是立即执行 // 协程恢复后: DefaultDispatcher-worker-1 <-- 挂起恢复后切换到其他线程

14、启动模式的面试回答要点:

  1. 明确说出4种模式:DEFAULT(默认,立即调度)、LAZY(懒惰,按需启动)、ATOMIC(原子,启动前抗取消)、UNDISPATCHED(未分发,立即在当前线程执行)。
  2. 重点对比 DEFAULT 和 LAZY:解释 LAZY 需要通过 start()join()/await() 来触发。
  3. 说明 UNDISPATCHED 的用途:强调它用于优化,让协程体在第一段非挂起代码中直接利用当前线程,避免一次线程切换的开销。在Android中,这可以用于在UI线程上立即执行一段设置UI状态的代码,然后再切换到后台线程。
  4. 强调 ATOMIC 的罕见性:指出它主要用于底层库的实现,普通业务代码几乎用不到,因为它会干扰协程的结构化取消。

15、Java中线程可以挂起吗?

这是一个很好的问题,答案是:技术上可以,但在现代Java编程实践中,这是被严格禁止和废弃的操作。

核心结论是:你不应该使用线程挂起,而应该使用协作式的等待/通知机制来达到类似“暂停”线程的效果。

正确的替代方案:让线程“协作式地等待”

你真正需要的不是“挂起”,而是让线程在某个条件满足前主动释放资源并进入等待状态。Java提供了多种安全机制:

目标场景推荐机制关键特点
等待一个特定的时间段Thread.sleep(long millis)让当前线程睡眠指定时间,不释放锁
等待另一个线程完成thread.join()当前线程等待目标线程终止。
线程间协作Object.wait() / notify()必须在synchronized块内调用,会释放锁并进入等待,直到被通知。
更现代的线程间协作java.util.concurrent (如 Condition, CountDownLatch, CyclicBarrier)功能更强大、更灵活、更安全的并发工具。

16、协程的“挂起”与线程挂起的本质区别

你之前问过协程,这里有一个重要对比:Kotlin协程的“挂起”和线程挂起是完全不同的概念

特性线程挂起 (suspend())协程挂起 (suspend 函数)
资源释放不释放锁,易导致死锁。释放底层线程,线程可去执行其他任务。
开销线程是重量级资源,被挂起依然占用系统资源。协程是轻量级用户态抽象,挂起几乎无开销。
机制由外部强制中断,是非协作的。协程内部主动让出,是协作式的。
状态管理危险且不可靠。安全,由协程框架自动管理。

协程挂起的本质:当一个协程调用挂起函数时,它并非被“卡住”,而是记录当前执行状态后暂停,并将它所在的线程释放回线程池。该线程可以立即去执行其他协程。当挂起操作完成(如网络请求返回),协程会被重新调度到空闲线程上恢复执行。这是一个非常高效的协作式多任务模型。

17、当被问及“线程可以挂起吗”,一个专业的回答应该是:

  1. 明确指出废弃性:技术上可以通过 Thread.suspend() 实现,但这是一个已被废弃的危险方法,因为它不释放锁,极易导致死锁。
  2. 提供替代方案:应该使用协作式的等待机制,如 Object.wait()/notify()Thread.sleep() 或更高级的 java.util.concurrent 工具。
  3. 对比协程(如果面试涉及Kotlin):可以进一步说明,协程的“挂起”是一个完全不同、安全且高效的概念,它通过释放线程来实现真正的非阻塞操作。

18、Flow 是什么?

Flow 是 Kotlin 协程库中用于处理异步数据序列的响应式流 API。它允许你以顺序化、声明式的方式处理多个按序到达的值,就像处理集合(Collections)一样,但是是异步的

三大核心特性:

  1. 冷流:默认的 Flow冷的。这意味着流的构建器代码(flow { ... } 块)只有在每次有收集者(调用 collect)时才会执行。每个收集者都会触发一次独立的执行流。
  2. 基于协程:Flow 完全构建在协程之上。它可以在挂起函数中执行,并可以优雅地处理生命周期和资源清理(通过结构化并发)。
  3. 丰富的操作符:提供类似 RxJava 的 map, filter, transform, zip, combine 等操作符,用于转换、组合和处理数据流。
// 创建一个 Flow
fun simpleFlow(): Flow<Int> = flow {
    for (i in 1..3) {
        delay(100) // 模拟异步工作
        emit(i)    // 发射一个值
    }
}

// 收集 Flow
fun main() = runBlocking {
    simpleFlow()
        .map { it * it } // 操作符:转换
        .filter { it > 2 } // 操作符:过滤
        .collect { value -> // 终端操作符:触发执行
            println(value)
        }
}

19、核心对比:Flow vs LiveData & 冷流 vs 热流

Flow 与 LiveData 的对比

特性Kotlin FlowAndroid LiveData
定位通用的异步数据流处理库(来自 Kotlin 协程库)。具有生命周期感知的数据持有者(Android 架构组件)。
上下文需要指定协程调度器(如 Dispatchers.IO)。主线程感知,自动将数据更新切换到主线程。
生命周期感知本身不感知,需配合 lifecycleScoperepeatOnLifecycle 安全收集。内置生命周期感知,自动在界面活跃时更新,避免内存泄漏。
值的历史冷流默认,热流(StateFlow)有。有。新观察者会立即收到最新数据。
适用场景复杂的异步数据流处理、网络请求链式调用、数据库变化监听等。简单的 UI 状态持有,用于在 View 和 ViewModel 之间通信。

最佳实践:在 Android 的 ViewModel 中,通常用 StateFlow (热流) 来暴露 UI 状态,因为它更像 LiveData;用普通的 Flow 处理仓库层的数据流。

20、冷流 (Cold Flow) 与热流 (Hot Flow) 对比

这个区别至关重要。

特性冷流 (如 flow { ... })热流 (如 StateFlow, SharedFlow)
生产启动时机每个收集者都会触发一次独立的生产生产独立于收集存在。有数据时就会生产,不管有无收集者。
数据共享不共享。每个收集者获得一套独立的数据序列。共享。所有收集者订阅同一个数据源。
典型代表flow { }, asFlow()StateFlow, SharedFlow, channel

如何将冷流变热? 使用 shareInstateIn 操作符。

21、Flow 的操作符与高阶用法

1. 操作符分类

  • 中间操作符:如 map, filter, transform, take。它们是的,返回一个新的 Flow,且不立即执行
  • 终端操作符:如 collect, single, first, toList。它们是的,会触发流的执行并消耗值。每个 Flow 必须有且仅有一个终端操作符。

2. 上下文切换:flowOn

flowOn 用于改变上游操作的协程上下文(如切换到 IO 线程)。

flow {
    // 这部分在 IO 线程执行
    emit(doNetworkRequest())
}
.map { ... } // 这部分也在 IO 线程
.flowOn(Dispatchers.IO)
.collect { // 这部分回到 collect 所在的上下文(通常是 Main)
    updateUI(it)
}

3. 异常处理

  • catch 操作符:捕获上游的异常,可以在此处发射一个兜底值。
flow { ... }
    .catch { e ->
        emit("fallback") // 捕获异常,发射一个替代值
    }
    .collect { ... }
  • 使用 try-catch 包裹 collect:可以捕获终端操作符中的异常。

22、高阶特性与面试难题

1. 背压问题与策略

背压:生产者发射数据的速度快于消费者处理数据的速度。Flow 提供了三种策略:

操作符 / 策略核心机制与数据命运内存影响适用场景代码示例
buffer()缓冲排队。生产者发出的数据先进入队列,消费者按自己的节奏从队列中取出处理。不丢弃数据,保证完整性。消耗内存建立缓冲区,默认大小为 kotlinx.coroutines.channels.defaultBuffer (64)。生产消费速率偶尔不匹配,且需保证数据完整性。例如,日志批量上报。flow.buffer().collect { ... }
conflate()合并丢弃。如果消费者来不及处理,中间值会被直接丢弃,只保留最新值供消费者处理。牺牲中间数据的完整性。内存开销极小,只保留一个最新值。只关心最新状态,中间状态可丢弃。例如,UI状态更新(如进度条)。flow.conflate().collect { ... }
collectLatest取新忘旧。当新值到达时,如果消费者还在处理旧值,会立即取消旧值的处理,转去处理最新值。内存开销小,但可能造成重复计算。处理计算密集型任务,且新数据使旧数据失效。例如,搜索建议实时联想。

2. StateFlowSharedFlow

  • StateFlow状态容器。必须有初始值,只保留最新值。新收集者会立即获得当前状态。用于替代 LiveData 管理 UI 状态。
// ViewModel 中
private val _uiState = MutableStateFlow<UiState>(UiState.Loading)
val uiState: StateFlow<UiState> = _uiState.asStateFlow()

// 更新状态
_uiState.value = UiState.Success(data)

SharedFlow事件广播器。没有初始值,可配置重播(replay)数量。用于一次性事件,如 toast 消息、导航命令。

private val _toastEvent = MutableSharedFlow<String>()
val toastEvent: SharedFlow<String> = _toastEvent.asSharedFlow()

// 发射事件
_toastEvent.emit("操作成功!")

23、当被问到“说说你对 Flow 的理解”时,建议按此逻辑展开:

  1. 定性:首先说明 Flow 是 Kotlin 协程库中处理异步数据流的响应式API,基于协程,支持顺序化、声明式编程。
  2. 核心:强调其冷流本质(生产由收集触发)和结构化并发带来的安全优势。
  3. 对比:主动与 LiveData 对比,指出 LiveData 是 Android 专属的、生命周期感知的状态持有者,而 Flow 是更通用的异步流工具,在 VM 层常用 StateFlow 暴露状态。
  4. 高阶:提到 背压处理buffer, collectLatest)和热流StateFlow/SharedFlow)的使用场景。
  5. 实践:最后落脚到 Android 架构,说明在 MVVM 中,用 Flow 处理数据层,用 StateFlow 在 ViewModel 中暴露 UI 状态,并在 UI 层使用 lifecycleScope.launchrepeatOnLifecycle(Lifecycle.State.STARTED) 安全收集。

准备一个实际案例:例如,描述一个搜索框,使用 debounce 防抖、filter 过滤空输入、flatMapLatest 发起网络请求、catch 处理异常、stateIn 转换为热流在 VM 中持有的完整流程。这能极大提升面试官对你的评价。

24、冷流和热流的区别?

简单来说:

  • 冷流:像点播视频。你(收集者)点开才会开始播放(生产数据),每个人看到的都是独立的流。
  • 热流:像电视直播。节目(数据)一直在播,不管有没有观众。观众随时加入(订阅)都能看到当前的画面(最新数据)。

冷流 vs 热流:全方位对比

特性维度冷流 (Cold Flow)热流 (Hot Flow)
生产启动时机“按需”生产:每次调用 .collect() 时,才会开始执行生产数据的代码。“独立”生产:生产者的生命周期独立于收集者。数据在生产,不管有没有人收集。
数据共享性“不共享”数据:每个收集者都会触发一次独立的数据生产流程,获得一套全新的、完整的数据序列“共享”数据源:所有收集者订阅的是同一个数据源,看到的是同一份数据流。
典型代表flow { ... }.asFlow()、大部分操作符产生的流。StateFlowSharedFlowMutableSharedFlowChannel
是否有初始值无此概念。从第一个 emit 开始。StateFlow 必须有初始值SharedFlow 无初始值,但可配置重播。
资源消耗每次收集都从头开始,可能重复执行昂贵操作(如网络请求)。数据源通常单例,避免重复开销,但需管理其生命周期。
适用场景计算新数据转换操作一次性异步序列(如单次网络请求结果流)。状态管理(UI状态)、事件广播(如Toast、导航命令)、跨组件共享实时数据

25、冷流和热流的回答

当被问到区别时,可以这样组织答案:

  1. 一句话比喻:冷流像点播,热流像直播
  2. 核心机制:冷流是 “数据生产依赖于收集” ,热流是 “数据生产独立于收集”
  3. 数据共享:冷流数据不共享,每次收集都是独立过程;热流共享同一数据源。
  4. 典型代表:冷流是 flow { ... };热流是 StateFlow (状态) 和 SharedFlow (事件)。
  5. 实战意义:在 Android 的 MVVM 中,数据层(Repository)通常暴露冷流,而 ViewModel 层使用 stateIn/shareIn 将其转换为热流StateFlow)暴露给 UI,这样能保证数据一致性和避免资源浪费。

26、Kotlin的结构化并发是什么意思?

结构化并发是 Kotlin 协程最核心的设计理念,它确保协程具有清晰的父子关系和生命周期管理

  • 父子协程关系
  • 作用域(CoroutineScope)
  • 结构化取消

1.父子协程关系:协程通过Job对象形成树状层级结构,子协程的生命周期完全由父协程控制。当父协程被取消时,所有子协程会自动取消,避免资源泄漏。`

2.协程作用域(CoroutineScope):每个协程代码块(如launch { ... })隐含一个CoroutineScope,它持有当前协程的上下文(包括Job和调度器)。作用域内的所有协程共享相同的生命周期。 结构化取消

3.结构化取消:取消操作会从父协程向子协程逐级传播,子协程的取消会触发父协程的取消回调,形成双向传播链

3e6fc6ca-8008-4c8b-9ba4-8fccd45110f5.png

下面我们通过具体场景和代码来理解这四大规则:

规则1:作用域负责其内部所有协程的生命周期
父协程(或作用域)在完成自己任务前,会挂起并等待所有子协程完成。这确保了没有“后台任务”会意外泄漏。

import kotlinx.coroutines.*

fun main() = runBlocking { // 这是一个根级作用域
    launch { // 子协程1
        delay(1000L)
        println("任务1完成")
    }
    launch { // 子协程2
        delay(2000L)
        println("任务2完成")
    }
    println("父协程即将等待...")
} // 在这里,runBlocking会等待两个launch都完成后才真正结束
// 输出:
// 父协程即将等待...
// 任务1完成
// 任务2完成
// (然后程序退出)

规则2:取消的自动传播
取消父协程会自动取消其所有子协程。这是避免资源浪费的关键。

fun main() = runBlocking {
    val parentJob = launch {
        launch { // 子协程A
            delay(3000L)
            println("子协程A完成") // 这行不会被执行
        }
        launch { // 子协程B
            delay(3000L)
            println("子协程B完成") // 这行也不会被执行
        }
    }
    delay(1000L)
    println("取消父协程")
    parentJob.cancelAndJoin() // 取消父协程
    println("程序结束")
}
// 输出:
// 取消父协程
// 程序结束
// (两个子协程被自动取消,没有输出)

规则3:错误的自动传播
一个子协程的失败(抛出未捕获异常)会导致其父协程和所有兄弟协程被取消。这防止了“部分崩溃、部分运行”的不一致状态。

fun main() = runBlocking {
    val handler = CoroutineExceptionHandler { _, exception ->
        println("捕获到异常: $exception")
    }
    val job = launch(handler) { // 父协程
        launch { // 兄弟协程1
            delay(1000L)
            println("兄弟1完成") // 这行不会被执行
        }
        launch { // 兄弟协程2(会失败)
            delay(500L)
            throw ArithmeticException("子协程计算失败!")
        }
    }
    job.join()
}
// 输出:
// 捕获到异常: java.lang.ArithmeticException: 子协程计算失败!
// (兄弟协程1也被取消了,没有输出)

规则4:统一的错误处理入口
通过 CoroutineExceptionHandler 或在父协程中用 try-catch 包裹 launch,可以集中处理所有子协程的异常,而不是在每个子协程里分别处理。

27、Kotlin结构化并发回答

四个核心优势:

  • 作用域会等待所有子协程完成;
  • 取消父协程会自动取消所有子协程;
  • 子协程的失败会自动向上传播;
  • 提供了统一的错误处理入口。

28、Android内存分配的位置

4f1e590e7b7a519ad023ae2f804adf1b.png

4ad60f28cfb56bd90e7bf0d61b4801c0.png