协程是什么
协程是一种轻量级的、用户态管理的线程,它允许函数在执行过程中被挂起,并在稍后恢复执行。
跟线程还有很大区别
|---------------|-------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------|----------------------------------------|
| 名称 | 工作方式 | 调度方式 | 资源消耗 |
| 线程(Thread) | 操作系统决定谁在CPU上跑,跑多久。线程可能在任意时刻被强制暂停(抢占),切到另一个线程。你作为厨师,是被动被拉去干活的。 | 线程的创建、销毁、切换都需要操作系统内核参与(内核态)。这涉及到CPU从用户态切换到内核态,开销较大。就像公司要招一个新员工,必须通过人事部走一遍流程。 | 一个线程通常占用几兆的内存,操作系统能创建的线程数量通常在几千个就算很多了。 |
| 协程(Coroutine) | 协程自己决定什么时候"让出"(Yield)执行权。它必须主动说:"我这会儿要等I/O,你先干别的。" 它不被别人强制暂停,而是主动挂起。你作为厨师,是主动切换工作的。 | 协程的调度完全在程序内部(用户态),由程序员或语言的运行时管理。创建和切换协程就像函数调用一样,没有内核参与,开销极小。就像团队内部临时组队干活,不需要人事部介入 | 一个协程只占用几KB的内存,一个程序中轻松创建数十万个协程没问题 |
PS:协程并不只有Kotlin才有的,例如,下面的编程语言也是有相关概念的(当然远远不只这些)。
|--------|--------------------------------------------------------------------------------------------|
| 编程语言 | 描述 |
| Python | 有asyncio库,通过async/await语法。但它的协程是单线程的,利用不了多核。真正的并行需要多进程 |
| Lua | 协程是语言的核心特性,非常优雅。 |
| Go | 不叫协程,叫Goroutine。它虽然是协程思想,但结合了多线程。Go的调度器会把成千上万个Goroutine,有效地分配给少数几个操作系统线程去跑。既有协程的轻量,又能利用多核。 |
| Java | 传统Java没有协程,依赖线程。但在Project Loom之后(JDK 21+),Java也引入了虚拟线程,本质就是协程。 |
| C++ | C++20也正式引入了协程。 |
协程可以解决什么问题
协程主要是为了解决"高并发"和"异步编程复杂化"这两个问题。
有一种异步代码使用同步来写的快感。下面使用跟java进行对比进行解释可以解决哪些痛点问题。
跟Java对比可以解决哪些痛点问题
痛点一:回调地狱 → 顺序书写
场景:连续调用三个API,后一个依赖前一个的结果。
java
// 典型的回调地狱 - 代码向右疯狂缩进
public void fetchUserData(String userId) {
apiClient.getUser(userId, new Callback<User>() {
@Override
public void onSuccess(User user) {
apiClient.getOrders(user.getId(), new Callback<List<Order>>() {
@Override
public void onSuccess(List<Order> orders) {
apiClient.getOrderDetails(orders.get(0).getId(),
new Callback<OrderDetails>() {
@Override
public void onSuccess(OrderDetails details) {
ui.update(details);
}
@Override
public void onFailure(Throwable t) {
handleError(t);
}
});
}
@Override
public void onFailure(Throwable t) {
handleError(t);
}
});
}
@Override
public void onFailure(Throwable t) {
handleError(t);
}
});
Java(CompletableFuture改进版):
java
// 好了不少,但异常处理和线程切换依然繁琐
public CompletableFuture<Void> fetchUserData(String userId) {
return apiClient.getUserAsync(userId)
.thenCompose(user -> apiClient.getOrdersAsync(user.getId()))
.thenCompose(orders -> apiClient.getOrderDetailsAsync(orders.get(0).getId()))
.thenAccept(details -> ui.update(details))
.exceptionally(throwable -> {
handleError(throwable);
return null;
});
}
Kotlin协程:
Kotlin
suspend fun fetchUserData(userId: String) {
try {
val user = apiClient.getUser(userId) // 挂起,不阻塞线程
val orders = apiClient.getOrders(user.id) // 顺序写,顺序执行
val details = apiClient.getOrderDetails(orders[0].id)
ui.update(details)
} catch (e: Exception) {
handleError(e)
}
}
核心差异:Kotlin用了 suspend 关键字,但 try-catch是同步代码的写法。你不必学习thenApply、thenCompose、exceptionally这一整套函数式API。
痛点二:线程昂贵 → 协程轻量
场景:需要处理10万个网络请求,每个请求耗时100ms。
Java线程模型:
java
// 1万个线程就能让JVM崩溃
ExecutorService executor = Executors.newFixedThreadPool(1000);
for (int i = 0; i < 100_000; i++) {
executor.submit(() -> {
httpClient.sendRequest(); // 线程阻塞在这里
return null;
});
}
// 问题:1000个线程占2GB内存,10万任务得排队几十分钟
Kotlin协程模型:
Kotlin
// 10万个协程轻松运行
val scope = CoroutineScope(Dispatchers.IO)
for (i in 0 until 100_000) {
scope.launch {
httpClient.sendRequest() // 协程挂起,不阻塞线程
}
}
// 真相:线程池可能只有64个线程,10万个协程在这64个线程上切换
// 内存占用:10万协程 ≈ 几十MB,10万线程 ≈ 几十GB
本质区别:
Java线程:一个请求占用一个线程,线程是操作系统资源
Kotlin协程:十万个请求复用几十个线程,协程是内存对象
痛点三:线程切换 → 一行代码
场景:网络请求必须在IO线程,结果解析必须在计算线程,UI更新必须在主线程。
Java线程:
java
public void loadAndShowImage(String url) {
CompletableFuture.supplyAsync(() -> {
// 1. IO线程下载
byte[] data = httpClient.download(url);
return data;
}, ioExecutor).thenApplyAsync(data -> {
// 2. 计算线程解码
Bitmap bitmap = decoder.decode(data);
return bitmap;
}, computeExecutor).thenAcceptAsync(bitmap -> {
// 3. 主线程显示
imageView.setImageBitmap(bitmap);
}, mainExecutor);
}
Kotlin协程:
Kotlin
suspend fun loadAndShowImage(url: String) {
// withContext = 切换线程,但代码不分裂
val data = withContext(Dispatchers.IO) {
httpClient.download(url)
}
val bitmap = withContext(Dispatchers.Default) {
decoder.decode(data)
}
withContext(Dispatchers.Main) {
imageView.setImageBitmap(bitmap)
}
}
关键洞察:Java的线程切换是把代码切成三段,Kotlin的线程切换只是函数内部的局部上下文变更,代码依然是完整一块。
痛点四:并发执行 & 超时控制
场景:同时请求两个API,取最快的结果,且必须在500ms内完成。
Java线程:
java
public void fetchFastest(String query) {
CompletableFuture<Result> future1 = apiClient.searchGoogleAsync(query);
CompletableFuture<Result> future2 = apiClient.searchBingAsync(query);
CompletableFuture.anyOf(future1, future2)
.orTimeout(500, TimeUnit.MILLISECONDS)
.thenAccept(result -> ui.showResult((Result) result))
.exceptionally(e -> {
ui.showTimeout();
return null;
});
}
// 问题:超时后后台线程还在跑,无法真正取消
Kotlin协程:
Kotlin
suspend fun fetchFastest(query: String) {
try {
val result = withTimeout(500) {
// 并发执行两个任务,取最快返回的那个
select<Result> {
async { googleApi.search(query) }.onAwait { it }
async { bingApi.search(query) }.onAwait { it }
}
}
ui.showResult(result)
} catch (e: TimeoutCancellationException) {
ui.showTimeout()
// 协程自动取消后台还在跑的任务
}
}
杀手锏:withTimeout 抛异常时,还在执行的任务会被自动取消。Java的 orTimeout 只是超时报错,线程依然在后台空跑。
痛点五:结构化并发(Kotlin独有)
这是Kotlin协程最超越Java的设计,不是语法糖,是并发管理范式的革新。
场景:一个界面需要同时拉取用户信息和会员等级,页面关闭时必须取消所有任务。
java
// 你需要自己维护任务清单
List<CompletableFuture<?>> tasks = new ArrayList<>();
public void loadUserData() {
CompletableFuture<User> userFuture = apiClient.getUserAsync();
CompletableFuture<VipInfo> vipFuture = apiClient.getVipInfoAsync();
tasks.add(userFuture);
tasks.add(vipFuture);
CompletableFuture.allOf(userFuture, vipFuture)
.thenAccept(v -> updateUI(userFuture.join(), vipFuture.join()));
}
// 页面销毁时,需要手动遍历取消
@Override
public void onDestroy() {
for (CompletableFuture<?> future : tasks) {
future.cancel(true); // 真的能取消吗?取决于实现
}
}
Kotlin
// 不需要任何手动维护
fun loadUserData() {
viewModelScope.launch { // viewModelScope = 结构化边界
val user = async { userRepo.getUser() }
val vip = async { vipRepo.getInfo() }
updateUI(user.await(), vip.await())
} // 当viewModelScope取消时,它下面所有的协程全部自动取消
}
结构化并发的本质:协程必须有一个父Scope,子协程的生命周期被父Scope管辖,形成树形结构。取消父节点,整棵树都取消。
|------|--------------|--------------|---------------|
| 维度 | Java | Kotlin | 差距 |
| 代码结构 | 回调嵌套/链式调用 | 顺序代码 | 读代码像读需求文档 |
| 资源消耗 | 1线程 ≈ 1~2MB | 1协程 ≈ 几KB 内存 | 占用降低99.9% |
| 取消机制 | 中断线程/协作取消 | 结构化取消 | 从手动管理到自动清理 |
| 学习曲线 | 熟悉线程池即可 | 需理解挂起点 | 陡峭,但回报极高 |
| 适用场景 | 任何场景 | IO密集型为主 | CPU密集仍用Java线程 |
协程使用的基本概念
先明确一个认知:协程不是线程的替代品,而是任务的编排器。掌握下面这几个用法,你已经能覆盖90%的生产场景。
引入依赖
Kotlin
// build.gradle.kts (Kotlin DSL)
dependencies {
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3")
// 如果你在Android
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3")
// 如果你在Spring Boot
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-reactive:1.7.3")
}
三把钥匙入门
协程世界里只有三个核心概念,记住它们就掌握了80%
|------------------|---------------------|-----------|
| 概念 | 是什么 | 像什么 |
| suspend | 挂起函数,可暂停并恢复 | 道路上的"待行区" |
| CoroutineScope | 协程的管辖范围,管理生命周期 | 公司的部门 |
| CoroutineContext | 协程的运行环境(线程池+异常处理器等) | 部门的资源配额 |
启动协程(两种姿势)
1. launch:即发即弃,不关心结果
Kotlin
fun main() = runBlocking { // runBlocking = 测试专用桥接器
// 启动一个协程
launch {
delay(1000) // suspend函数,挂起1秒
println("World!") // 第二行打印
}
println("Hello,") // 第一行打印
delay(1500) // 等待协程执行完
}
2. async:有返回值,需要等待结果
Kotlin
fun main() = runBlocking {
val deferred: Deferred<Int> = async {
delay(1000)
return@async 42 // 返回结果
}
println("Waiting...")
val result = deferred.await() // 挂起直到拿到结果
println("Answer: $result")
}
// 输出:
// Waiting...
// (1秒后)
// Answer: 42
核心区别:
launch 返回 Job(任务句柄,可取消)
Kotlin
async 返回 Deferred<T>(带结果的Job)
### 调度器(切换线程)
协程不绑定线程,但你得告诉它去哪儿跑:
/ Dispatchers 是三选一的单选题
suspend fun demoDispatchers() {
// 1. 主线程(Android UI/JavaFX)------ 更新UI
withContext(Dispatchers.Main) {
textView.text = "加载完成"
}
// 2. IO线程池(网络、文件、数据库)------ 默认64线程
val data = withContext(Dispatchers.IO) {
URL("https://example.com").readText()
}
// 3. 默认线程池(CPU密集型计算)------ 等于CPU核心数
val result = withContext(Dispatchers.Default) {
(1..1000000).filter { it % 2 == 0 }.sum()
}
// 4. 单线程上下文(特殊场景)
val singleThread = Dispatchers.IO.limitedParallelism(1)
}
实战口诀:IO密集用 Dispatchers.IO,CPU密集用 Dispatchers.Default,改UI用 Dispatchers.Main
结构化并发(最重要)
错误示范(全局协程=内存泄漏):
Kotlin
// ❌ 极度危险!协程会永远运行
GlobalScope.launch {
while (true) {
delay(1000)
println("我还在跑...")
}
}
正确示范:
Kotlin
class MyViewModel {
// 1. 创建自己的Scope,绑定生命周期
private val scope = CoroutineScope(Dispatchers.Main + SupervisorJob())
fun loadData() {
// 2. 在这个Scope里启动协程
scope.launch {
val data = withContext(Dispatchers.IO) { fetchApi() }
showData(data)
}
}
fun onClear() {
// 3. 清理时取消所有协程
scope.cancel()
}
}
Android专属:
Kotlin
// Android已经给你封装好了,直接用
class MyFragment : Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
// viewLifecycleOwner.lifecycleScope 是Activity/Fragment自带的
viewLifecycleOwner.lifecycleScope.launch {
// 当View销毁时,这个协程自动取消
}
}
}
class MyViewModel : ViewModel() {
fun load() {
// viewModelScope 是ViewModel自带的
viewModelScope.launch {
// 当ViewModel清空时,自动取消
}
}
}
异常处理(三板斧)
第一板斧:try-catch(最常用)
Kotlin
scope.launch {
try {
val result = withContext(Dispatchers.IO) {
riskyNetworkCall()
}
} catch (e: IOException) {
// 网络错误,降级处理
showErrorToast()
}
}
第二板斧:CoroutineExceptionHandler(全局兜底)
Kotlin
val exceptionHandler = CoroutineExceptionHandler { _, throwable ->
Log.e("Coroutine", "未捕获异常", throwable)
// 上报Bugly/Sentry
}
val scope = CoroutineScope(
Dispatchers.Main +
SupervisorJob() +
exceptionHandler // 加进去就行
)
第三板斧:supervisorScope(儿子犯错不连累父亲)
Kotlin
supervisorScope {
// 这两个async是兄弟,一个失败不影响另一个
val deferred1 = async { riskyTask1() }
val deferred2 = async { safeTask2() }
try {
deferred1.await()
} catch (e: Exception) {
println("Task1失败了,但Task2还能拿结果")
}
println(deferred2.await()) // 依然能拿到
}
coroutineScope:子协程失败,父协程也失败(严格)
supervisorScope:子协程互不影响(宽容)
常用操作符(日常兵器库)
1. delay ------ 非阻塞等待
// Java的Thread.sleep()会阻塞线程
// Kotlin的delay()只挂起协程,不阻塞线程
2. withTimeout ------ 超时即取消
Kotlin
try {
withTimeout(3000) { // 3秒超时
fetchData()
}
} catch (e: TimeoutCancellationException) {
println("超时了,协程已自动取消")
}
3. join/await ------ 等待协程结束
Kotlin
val job = scope.launch {
delay(1000)
println("Done")
}
job.join() // 挂起,等上面的协程执行完
println("Finished") // 上面打印完才会执行这里
4. cancel ------ 取消协程
Kotlin
val job = scope.launch {
repeat(1000) { i ->
delay(500)
println("进度: $i")
}
}
delay(2000)
job.cancel() // 发送取消信号
job.join() // 等待确认取消
// 或者直接用 job.cancelAndJoin()
5. flow ------ 协程版RxJava
Kotlin
fun listenMessages(): Flow<String> = flow {
while (true) {
delay(1000)
emit("新消息: ${System.currentTimeMillis()}") // 发射数据
}
}
// 使用
lifecycleScope.launch {
listenMessages()
.flowOn(Dispatchers.IO) // 上游在IO线程
.catch { e -> println("错误") } // 异常处理
.collect { message -> // 下游在主线程
textView.text = message
}
}
总结:
上面列举的是学习kotlin需要掌握的核心知识点,后续后分模块跟大家一起探讨相关的细节。共同学习共同进步。