android Kotlin协程系列一

49 阅读9分钟

一 协程基础认知 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. 等水烧开后泡茶(这需要第1步的结果)
  4. 吃早餐(这需要第2步和第3步都完成)

  1. 普通函数(阻塞式)的糟糕做法

如果用普通的同步代码来写,就像是一个固执的人:

fun makeBreakfast() {
    val water = boilWater() // 站在水壶前死死地盯着,等10分钟,什么都不做
    val tea = makeTea(water) // 水开了,开始泡茶
    val bread = cutBread() // 开始切面包
    eat(tea, bread) // 吃早餐
}

问题:在等水烧开的那10分钟里,这个人(主线程)就傻站着,不能去切面包,效率极低。如果这是在 Android 应用里,整个界面都会卡住。


  1. 使用挂起函数(非阻塞式)的聪明做法

现在,我们换一个聪明人,他懂得“挂起”和“切换”:

// 挂起函数:告诉系统这是一个需要时间的活儿,可以把我“挂起”,等好了再“恢复”
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("开始吃早餐!")
}

关键点解释(通俗版):

  1. 挂起: 当执行到 boilWater() 或 waterDeferred.await() 这些挂起函数时,这个函数(或者说当前的协程)会说:“我需要等一会儿,别占着茅坑不拉屎,你先去执行别的任务吧”。于是它自己就“挂”起来了,让出了线程。
  2. 非阻塞: 在等水烧开(boilWater)的10分钟里,线程没有被阻塞。它空闲出来可以去执行 cutBread() 任务了。这就好比你在等水开的时候,没有傻站着,而是利用这个等待时间去切面包了。
  3. 恢复: 当水烧好了(boilWater 的 delay 时间到了),或者 async 的任务完成了,系统会说:“嘿,你等的东西好了”,然后协程会从它刚才被挂起的地方 (await()) 后面恢复执行,继续泡茶。
  4. “看起来”是同步的代码: 注意看 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(调度器)这些组件共同支撑起协程的定义-启动-执行-暂停-恢复全流程。