Kotlin初入协程

协程是什么

协程是一种轻量级的、用户态管理的线程,它允许函数在执行过程中被挂起,并在稍后恢复执行。

跟线程还有很大区别

|---------------|-------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------|----------------------------------------|
| 名称 | 工作方式 | 调度方式 | 资源消耗 |
| 线程(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需要掌握的核心知识点,后续后分模块跟大家一起探讨相关的细节。共同学习共同进步。

相关推荐
codeGoogle1 小时前
2026 年 IM 怎么选?聊聊 4 家主流即时通讯方案的差异
android·前端·后端
特立独行的猫a2 小时前
腾讯Kuikly框架实战:基于腾讯Kuikly框架实现Material3风格底部导航栏
android·harmonyos·compose·kmp·实战案例·kuikly
半切西瓜2 小时前
Android Studio ViewBinding绑定视图控件
android·ide·android studio
奔跑吧 android4 小时前
【车载audio】【audio hal 01】【Android 音频子系统:Audio HAL Server 启动全流程深度解析】
android·音视频·audio·audioflinger·aosp15·车载音频·audiohal
似霰4 小时前
Android 日志系统6——logd 读日志过程分析
android·log
技术摆渡人4 小时前
Android CPU调度优化完整剖析指南
android
雪球Snowball4 小时前
【Android关键流程】Window相关类及属性
android
我命由我123454 小时前
Android多进程开发 - AIDL 最简单的实现、传递数据大小限制
android·java·java-ee·kotlin·android studio·android jetpack·android-studio
冬奇Lab11 小时前
Android系统启动流程深度解析:从Bootloader到Zygote的完整旅程
android·源码阅读