一 协程基础认知 1.1 Kotlin协程是什么?它和Java中的线程有啥本质区别?
Kotlin协程是一种运行在用户态的轻量级"任务执行单元",本质是由Kotlin编译器和Kotlin runtime(而非操作系统内核)管理的"协作式"任务调度机制,它允许在单线程内通过"暂停-恢复"机制实现多任务切换,从而达到并发效果。
与Java线程的核心区别:
管理层面:Java线程是"内核态线程",由操作系统内核调度(创建,切换,销毁均需内核参与),协程是"用户态任务",由Kotlin runtime(如kotlin-coroutines库)调度,不依赖内核。
资源开销:Java线程占用MB级内存(如默认栈大小1MB),且线程切换需保存/恢复寄存器等内核状态,开销大。协程仅占用kb级内存,切换由用户态代码控制(保存少量局部变量和状态),开销极低。
并发能力:Java中创建数千个线程会导致OOM或调度性能急剧下降,协程可轻松创建数十万个,适合高并发场景。
总结:线程是"重量级的内核调度单元",协程是"轻量级的用户态任务"。
1.2为什么需要Kotlin协程?它能解决Java并发编程中的哪些痛点?
Kotlin协程的设计初衷是简化异步并发编程,解决Java中以下核心痛点:
回调地域:
Java中异步操作(如网络请求,文件读写)需通过回调实现,多层嵌套会导致代码可读性极差(如new Callback(){ onSuccess(()->new Callback(){...}) }),协程允许用同步代码的写法实现异步逻辑(通过suspend函数暂停,恢复后继续执行),彻底消除嵌套。
线程切换繁琐:
Java中线程切换需手动管理线程池(如ExectorService),并通过post等方法切换到UI线程(Android中),代码冗余且易出错,协程通过Dispather和withContext可自动完成线程切换(如从IO线程切回主线程),无需手动处理。
线程资源浪费:
Java中IO操作会阻塞线程,导致线程在等待期间完全闲置(却还占用内存和内核资源),协程在IO等待时会主动"暂停"并让出线程,让线程去执行其他任务,IO完成后再"恢复",大幅提高线程利用率。
并发任务协调复杂
Java中协调多个并发任务(如等待多个接口返回后合并结果)需要CountDownLatch,CompleteableFuture等工具,代码复杂,协程通过async/await可简洁地实现多任务并行与结果聚合,逻辑更直观。
1.3 协程的"非阻塞"特性是如何实现的?它和Java中"线程阻塞"的区别是什么?
协程的"非阻塞"本质是让出执行权+状态保存与恢复,而Java线程的阻塞是内核强制挂起,二者机制完全不同。
协程非阻塞的实现逻辑,当协程执行到suspend函数时,会触发以下操作:
保存状态:Kotlin编译器会将协程的局部变量,执行位置等信息保存到一个状态机中. 主动让出线程:协程通知调度器我需要等待,先让出线程,此时线程会被释放,去执行其他任务。 当等条件满足(如delay时间到,或者异步任务执行完了),调度器会找到空闲线程,从保存的状态中恢复协程执行,继续往下执行。
这里举个生活中例子:来解释 Kotlin 中的挂起函数非阻塞特性。
场景:早上做早餐
想象一下你早上做早餐的流程:
- 烧水(这是一个需要等待的“耗时任务”)
- 切面包(这是一个可以立刻做的“快速任务”)
- 等水烧开后泡茶(这需要第1步的结果)
- 吃早餐(这需要第2步和第3步都完成)
- 普通函数(阻塞式)的糟糕做法
如果用普通的同步代码来写,就像是一个固执的人:
fun makeBreakfast() {
val water = boilWater() // 站在水壶前死死地盯着,等10分钟,什么都不做
val tea = makeTea(water) // 水开了,开始泡茶
val bread = cutBread() // 开始切面包
eat(tea, bread) // 吃早餐
}
问题:在等水烧开的那10分钟里,这个人(主线程)就傻站着,不能去切面包,效率极低。如果这是在 Android 应用里,整个界面都会卡住。
- 使用挂起函数(非阻塞式)的聪明做法
现在,我们换一个聪明人,他懂得“挂起”和“切换”:
// 挂起函数:告诉系统这是一个需要时间的活儿,可以把我“挂起”,等好了再“恢复”
suspend fun boilWater(): Water {
// 模拟耗时,比如10分钟
delay(10000) // delay 是一个内置的挂起函数
println("水烧开了!")
return Water()
}
// 普通函数或挂起函数都可以
suspend fun cutBread(): Bread {
delay(2000) // 切面包需要2分钟
println("面包切好了!")
return Bread()
}
// 主流程也是一个挂起函数
suspend fun main() {
println("开始做早餐")
// 启动一个“烧水”的协程
val waterDeferred = async { boilWater() } // async 启动一个新的协程
// 在等水开的时候,我不阻塞,我可以立刻去“切面包”
val bread = cutBread() // 这是一个顺序调用,会花2分钟
// 现在需要用水了,但如果水还没烧开,我就在这里“挂起”等待
val water = waterDeferred.await() // await() 是一个挂起函数
val tea = makeTea(water)
println("开始吃早餐!")
}
关键点解释(通俗版):
- 挂起: 当执行到 boilWater() 或 waterDeferred.await() 这些挂起函数时,这个函数(或者说当前的协程)会说:“我需要等一会儿,别占着茅坑不拉屎,你先去执行别的任务吧”。于是它自己就“挂”起来了,让出了线程。
- 非阻塞: 在等水烧开(boilWater)的10分钟里,线程没有被阻塞。它空闲出来可以去执行 cutBread() 任务了。这就好比你在等水开的时候,没有傻站着,而是利用这个等待时间去切面包了。
- 恢复: 当水烧好了(boilWater 的 delay 时间到了),或者 async 的任务完成了,系统会说:“嘿,你等的东西好了”,然后协程会从它刚才被挂起的地方 (await()) 后面恢复执行,继续泡茶。
- “看起来”是同步的代码: 注意看 main 函数里的代码,它写起来就像是同步的顺序代码一样,非常直观(先烧水,然后切面包,然后等水…)。但底层却是非阻塞的、异步执行的。这正是挂起函数和协程的强大之处:用同步的方式写异步代码。
总结比喻:
挂起函数就像是一个聪明的管家。
· 你(主线程)让管家(协程)去烧水(挂起函数)。 · 管家不会让你在旁边干等,他会把水壶放在炉子上,然后告诉你:“主人,您可以去切面包了,水好了我叫您。” · 于是你去切面包了(线程去执行其他任务)。 · 水烧开了,管家来通知你:“主人,水好了。”(恢复) · 你接着用水去泡茶。
这个过程中,你(主线程)一直没有闲着,效率非常高。这就是挂起函数的精髓。
与Java线程阻塞的区别:
维度 : Java线程阻塞(Thread.sleep(1000)) Kotlin协程非阻塞
线程状态 线程进入阻塞态,被内核挂起 线程始终处于运行态,执行其他任务 资源占用 线程在阻塞期间还占用内存和资源 协程暂停时不占用线程资源 调度参与方 由操作系统内核调度(强制挂起/唤醒) 由用户态代码(协程调度器)控制 性能影响 大量阻塞线程会导致资源压力和调度压力 线程利用率极高,适合高并发场景
1.4 用个简单例子说明协程的哪些核心组成部分?
fun main() {
runBlocking {
launch {
delay(1000)
println("协程1执行完成")
}
launch {
delay(3000)
println("协程2执行完成")
}
println("主线程继续执行")
}
println("所有协程完成后打印")
}
代码中的核心组成部分: CoroutineScope(协程作用域):作用域的核心作用是管理协程生命周期(如取消所有子协程)。
协程启动器(launch):用于创建并启动协程的函数,这里launch会创建一个无返回值的协程,类似的启动器还有async(用于有返回值的协程)。
suspend函数(暂停函数):delay(1000)是一个suspend函数,它会暂停当前协程的执行(而非阻塞线程),是协程实现非阻塞的核心入口。
Dispather(调度器)可通过Dispatchers.IO,Dispatchers.Main等指定协程运行的线程。
协程基础认知总结:
协程的本质与线程的区别:协程是用户态轻量级任务,由Kotlin runtime调度,而非操作系统内核,与线程相比,它内存开销极低(Kb级vs线程的Mb级),切换成本几乎可以忽略,能支持数十万级并发,解决了线程在高并发场景下的资源瓶颈,其核心差异:线程是内核强制调度的重量级单元,协程是用户态协作式的轻量级任务,协程运行在线程之上,通过更高效的暂停-恢复机制实现并发。
协程解决的Java并发痛点:针对Java异步编程的四大核心问题提供了优雅解决方案:用同步代码写法消除回调地域,通过Dispather和withContext自动完成线程切换,替代手动线程池管理,IO等待时主动让出线程,解决线程闲置浪费问题,用async/awit简洁实现多任务并行与结果聚合,替代CountDonwLatch等复杂工具。
非阻塞特性的实现逻辑:协程的非阻塞源于主动让出+状态保存与恢复,执行到suspend函数时,先保存局部变量和执行位置到状态机,再主动让出线程(让线程执行其他任务),等待条件满足后,从保存的状态恢复执行,这与Java线程阻塞时内核强制挂起,线程闲置的机制完全不同,大幅提升了线程利用率。
一个简单协程示例包含四大核心元素:CoroutineScope(作用域,管理生命周期),协程启动器(如launch,创建无返回值协程),suspend函数,Dispather(调度器)这些组件共同支撑起协程的定义-启动-执行-暂停-恢复全流程。