线程与协程的协作与等待:简明总结
在多线程编程中,实现“线程等待其他线程执行完成”通常需要使用各种同步API,比如:
- Thread.join()
让一个线程等待另一个线程结束。 - Future / CompletableFuture
通过get()或回调的方式等待异步结果。 - CountDownLatch
让一个或多个线程等待其他线程全部完成。
这些API用起来复杂且容易出错。
协程的等待和协作——更简单
Kotlin协程原生支持等待/协作,只需用如下API即可完成所有线程世界里的等待/协作场景:
1. Job.join()
让一个协程挂起自己,直到另一个协程完成,不会阻塞线程:
val job1 = scope.launch {
// ... do work ...
}
scope.launch {
job1.join() // 挂起自己,等待job1结束
// ... do something after job1 ...
}
2. Deferred.await()
如果需要等协程返回结果,用async创建,然后await()获取结果,同时挂起自己:
val deferred = scope.async {
// ... do work and return a result ...
"result"
}
scope.launch {
val result = deferred.await() // 等待结果
// ... use result ...
}
3. 多协程等待(全部完成)
用多个Job.join()或多个Deferred.await()即可实现协程版的CountDownLatch,无需额外同步工具:
val job1 = scope.launch { ... }
val job2 = scope.launch { ... }
scope.launch {
job1.join()
job2.join()
// 只有都完成才会继续
}
或
val d1 = scope.async { ... }
val d2 = scope.async { ... }
scope.launch {
val r1 = d1.await()
val r2 = d2.await()
// 两个结果都拿到后再处理
}
总结
- 线程中等待/协作需要各种同步API,复杂且容易阻塞。
- 协程中只需
join()和await(),挂起而不阻塞线程,天然支持并发协作,写法简单,语义清晰。 - 协程天然支持结构化并发,代码更安全、更高效。
Java线程协作与等待机制对比
1. Thread.join() 例子
java
复制编辑
public class ThreadJoinExample {
public static void main(String[] args) throws InterruptedException {
Thread thread1 = new Thread(() -> {
System.out.println("Thread1 start");
try { Thread.sleep(1000); } catch (Exception ignored) {}
System.out.println("Thread1 end");
});
Thread thread2 = new Thread(() -> {
System.out.println("Thread2 start");
try { Thread.sleep(1500); } catch (Exception ignored) {}
System.out.println("Thread2 end");
});
thread1.start();
thread2.start();
thread1.join(); // 等thread1结束
thread2.join(); // 再等thread2结束
System.out.println("Main thread resumes after both are done.");
}
}
输出示例:
sql
复制编辑
Thread1 start
Thread2 start
Thread1 end
Thread2 end
Main thread resumes after both are done.
说明: join()让主线程阻塞,直到指定线程结束。
2. Future 示例
import java.util.concurrent.*;
public class FutureExample {
public static void main(String[] args) throws Exception {
ExecutorService pool = Executors.newFixedThreadPool(2);
Future<Integer> f1 = pool.submit(() -> {
Thread.sleep(1000);
return 1;
});
Future<Integer> f2 = pool.submit(() -> {
Thread.sleep(1500);
return 2;
});
System.out.println("Result1: " + f1.get()); // 阻塞直到结果返回
System.out.println("Result2: " + f2.get());
pool.shutdown();
}
}
输出示例:
Result1: 1
Result2: 2
说明: get()阻塞等待任务返回。
3. CompletableFuture 示例
import java.util.concurrent.*;
public class CompletableFutureDemo {
public static void main(String[] args) throws InterruptedException {
CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> {
System.out.println("Task running in " + Thread.currentThread().getName());
return 10;
});
future.thenApply(x -> x * 2)
.thenAccept(result -> System.out.println("Result: " + result));
Thread.sleep(200); // 保证异步回调输出
}
}
输出示例:
Task running in ForkJoinPool.commonPool-worker-1
Result: 20
说明: 支持非阻塞链式回调,主线程不用等结果,任务完了自动触发。
4. CountDownLatch 等待多个线程
import java.util.concurrent.CountDownLatch;
public class LatchExample {
public static void main(String[] args) throws InterruptedException {
CountDownLatch latch = new CountDownLatch(2);
new Thread(() -> {
System.out.println("Thread1 work");
latch.countDown();
}).start();
new Thread(() -> {
System.out.println("Thread2 work");
latch.countDown();
}).start();
latch.await(); // 阻塞直到计数为0
System.out.println("All done.");
}
}
输出示例:
Thread1 work
Thread2 work
All done.
说明: 等待多个线程全部执行完再继续。
总结&对比
- Thread.join/Future.get:阻塞等待,最基础。这是通过线程间的同步原语(如
wait()和notify())实现的。 - CompletableFuture:非阻塞回调,适合任务链/组合。它通过
ForkJoinPool或自定义的线程池执行任务,并在任务完成时自动触发回调。 - CountDownLatch:阻塞等待一组线程全部完成。
协程里的等价方案(Kotlin为例)
val job1 = launch { /*...*/ }
val job2 = launch { /*...*/ }
job1.join() // 挂起等待,不阻塞线程
job2.join()
// 或者 async/await
val d1 = async { /*...*/ }
val d2 = async { /*...*/ }
println(d1.await())
println(d2.await())
- 协程的join/await都是挂起式,不会阻塞线程,写法比Java线程友好得多。
协程等待多个协程结果的方式
1. 多个 Job 的 join(适用于只等结束,不关心结果)
val job1 = launch { delay(1000); println("job1 finished") }
val job2 = launch { delay(2000); println("job2 finished") }
launch {
job1.join() // 等待job1结束
job2.join() // 等待job2结束
println("All jobs finished.")
}
核心: join()让当前协程挂起直到目标Job结束,不阻塞线程,可串联等待多个Job。
2. async/await 等待多个结果(等结果 + 按需处理)
val d1 = async { delay(1000); "Result1" }
val d2 = async { delay(2000); "Result2" }
val d3 = async { delay(1500); "Result3" }
val r1 = d1.await() // 挂起直到d1返回
val r2 = d2.await()
val r3 = d3.await()
println("$r1 $r2 $r3")
核心: async返回Deferred,await()挂起当前协程直到有结果,同时还能拿到返回值。常用于并发计算、聚合结果。
3. Channel模拟CountDownLatch(等N个信号,不关心内容,最接近CountDownLatch语义)
val channel = Channel<Unit>(2)
launch {
repeat(2) { channel.receive() } // 等两次
println("All workers done.")
}
launch {
delay(1000)
channel.send(Unit) // worker1完成信号
}
launch {
delay(2000)
channel.send(Unit) // worker2完成信号
}
核心: Channel容量设为N,N个子任务完成后各自send(Unit),主协程repeat(N) receive()即可等全部子任务完成。
总结/对比
- Job.join:等一组协程执行完(不关心返回值)
- async/await:并发等一组协程,能拿到每个结果
- Channel+repeat:用信号计数,模拟CountDownLatch的“等N个点完成”
- 优势:协程的等待/聚合是挂起,不会真正阻塞线程,写法比Java原生线程清晰太多。
一句话总结:
“协程的join、await和Channel模拟Latch,覆盖了Java线程协作的全部常用场景,并且更优雅、易控、不会阻塞。”
协程 select:谁先到谁赢,处理并发“最快结果”
select 可以理解为协程世界的“最快响应多路复用器” ,你监控一堆“可能完成的异步任务”,谁先完成就立刻处理,并且其它没完成的操作直接被取消。
1. 多通道竞争的真实场景
假如要请求多个服务器的某个资源,只要有一个返回结果就行了,其余的就不用管了。这种情况用 select 再合适不过。
示例代码(通道竞争):
import kotlinx.coroutines.*
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.selects.select
fun main() = runBlocking {
val server1 = Channel<String>()
val server2 = Channel<String>()
// 分别模拟两个服务器异步响应
launch {
delay(300) // 300ms后返回
server1.send("服务器1返回:数据A")
}
launch {
delay(150) // 150ms后返回
server2.send("服务器2返回:数据B")
}
// select 等待多个通道,谁先返回谁赢
val result = select<String> {
server1.onReceive { data ->
println("[select] 来自server1: $data")
data
}
server2.onReceive { data ->
println("[select] 来自server2: $data")
data
}
}
println("最终拿到结果:$result")
}
输出分析:
[select] 来自server2: 服务器2返回:数据B
最终拿到结果:服务器2返回:数据B
只有最快的那一个分支会被执行!
2. 监听 Job 或 Deferred:只等第一个“完成的人”
如果有多个后台任务,谁先干完就用谁的结果,select 支持直接监听 Job 的 onJoin 或 Deferred 的 onAwait 。
onJoin 和 onAwait
- 这两个方法只能在
select {}代码块里用,不能直接调用。 - 它们不是挂起当前协程,而是“注册一个监听器”:
“当 job/deferred 完成时,马上执行你写在大括号里的代码,并把返回值当作 select 的最终返回值。” select {}代码块只能用onJoin、onAwait这类专门为 select 设计的“事件监听器”式 API,不能直接用join()或await()。
示例代码(Job/Deferred竞争):
import kotlinx.coroutines.*
import kotlinx.coroutines.selects.select
fun main() = runBlocking {
val job1 = launch {
delay(500)
println("job1 完成")
}
val deferred2 = async {
delay(300)
println("deferred2 完成")
"deferred2结果"
}
val deferred3 = async {
delay(800)
println("deferred3 完成")
"deferred3结果"
}
val fastest = select<String> {
job1.onJoin {
println("[select] job1 完成被监听")
"job1无结果"
}
deferred2.onAwait { value ->
println("[select] deferred2 完成被监听,值=$value")
value
}
deferred3.onAwait { value ->
println("[select] deferred3 完成被监听,值=$value")
value
}
}
println("最快完成的结果:$fastest")
}
输出分析:
deferred2 完成
[select] deferred2 完成被监听,值=deferred2结果
job1 完成
deferred3 完成
最快完成的结果:deferred2结果
只有最快完成的那一个分支才会真正决定 select 的返回值。其它的不会再被处理。
3. 增加超时防护 onTimeout
真实业务不会傻等,有时候需要超时兜底。
import kotlinx.coroutines.*
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.selects.select
import kotlinx.coroutines.selects.onTimeout
fun main() = runBlocking {
val ch1 = Channel<String>()
val ch2 = Channel<String>()
// 故意不发送数据,模拟超时
val result = select<String> {
ch1.onReceive {
"ch1返回:$it"
}
ch2.onReceive {
"ch2返回:$it"
}
onTimeout(200) {
println("[select] 超时了,走兜底逻辑")
"TIMEOUT"
}
}
println("select返回:$result")
}
输出分析:
[select] 超时了,走兜底逻辑
select返回:TIMEOUT
4. select 的核心机制说明
- 多路选择:监听一堆 Channel、Job、Deferred、超时……只处理最快完成的那一个。
- 只返回一个结果:无论多少分支,select 只返回第一个完成的结果,其他分支自动取消。
- 业务价值:高并发下的最快响应场景、竞争请求、优先级处理、超时容错、批量取消。
总结
- select = 谁先完成用谁的,其他都不用管
- 只需要写一次 select 代码块,就能监听多种类型的异步结果(Channel/Job/Deferred/Timeout)
- 实际开发中常用于“抢首个响应”,“兜底超时”,“多源数据竞争”场景
学后检测
一、单选题(共8题)
1. 在Kotlin协程中,让一个协程等待另一个协程结束,最常用的API是?
A. await()
B. join()
C. get()
D. latch()
【答案】B
【解析】Job.join()挂起当前协程直到目标协程结束,是最常用的等待手段。await()适用于async返回的Deferred。
2. 下列关于Java Thread.join()说法正确的是?
A. join是非阻塞的
B. join会导致调用者线程阻塞直到目标线程终止
C. join会取消目标线程
D. join只能用于主线程
【答案】B
【解析】join会让调用者阻塞,直到目标线程执行完毕。不会取消线程,也不限主线程。
3. Java中,用于一个线程等待多个线程全部完成的常用同步工具类是?
A. Future
B. CompletableFuture
C. CountDownLatch
D. CyclicBarrier
【答案】C
【解析】CountDownLatch允许主线程等待多个子线程全部完成。
4. Kotlin协程中,获取async结果且挂起自己直到结果可用的方法是?
A. get()
B. join()
C. await()
D. result()
【答案】C
【解析】Deferred.await()用于获取async结果且挂起等待。
5. 下列关于select在Kotlin协程中的作用,哪项描述错误?
A. select可同时监听多个挂起操作
B. select会选最先完成的操作并返回结果
C. select只能监听channel
D. select支持onTimeout回调
【答案】C
【解析】select可监听job、deferred、channel等挂起操作,不只支持channel。
6. 下列关于Java Future.get()和CompletableFuture.thenApply()说法正确的是?
A. get()阻塞当前线程直到有结果
B. thenApply()是阻塞调用
C. get()和thenApply()都可非阻塞
D. thenApply()是回调式非阻塞链式处理
【答案】A、D
【解析】get()阻塞,thenApply()为非阻塞式回调链式操作。
7. Kotlin协程中用Channel实现“等待N个子任务全部完成”,关键做法是什么?
A. 设置Channel容量为1
B. 重复receive N次
C. launch N个协程都send到Channel
D. B+C
【答案】D
【解析】用Channel容量为N,N个子任务完成各自send一次,主协程receive N次即可。
8. 下列关于select中onTimeout用法,描述正确的是?
A. 用于设置超时后回调
B. 必须监听job
C. 超时时间内无挂起操作完成则触发
D. 只能监听channel
【答案】A、C
【解析】onTimeout设置超时逻辑,无操作完成时触发,与监听类型无关。
二、多选题(共6题)
1. 下列哪些操作可用于Kotlin协程等待多个协程全部完成?
A. 多个Job.join()
B. 多个Deferred.await()
C. select
D. Channel+receive循环
【答案】A、B、D
【解析】A/B为常规等所有子任务完成,select适合“最快完成即继续”,D是channel实现的CountDownLatch等价用法。
2. select{}代码块中可以监听哪些事件?
A. job1.onJoin{}
B. deferred.onAwait{}
C. channel.onReceive{}
D. onTimeout{}
【答案】A、B、C、D
【解析】文章讲解,select支持job、deferred、channel各种onXXX及onTimeout。
3. Java的CountDownLatch机制和下列哪些Kotlin协程写法等价?
A. 多个launch各自send到Channel,主协程循环receive N次
B. 主协程顺序join N个Job
C. 主协程await所有Deferred
D. 用select{}监听所有子任务onJoin{}
【答案】A、B、C
【解析】A/B/C都能达到“主协程等待所有子任务”效果,select适合最快的那一个。
4. 关于Java CompletableFuture,以下说法正确的是?
A. 支持回调链
B. 必须阻塞主线程才能拿到结果
C. 可以组合多个异步任务
D. 支持异步与同步两种处理模式
【答案】A、C、D
【解析】CompletableFuture支持回调链和组合异步处理,也可以同步get阻塞主线程,但不是必须。
5. select和CountDownLatch等价点与不同点有哪些?
A. select能等任意数量的事件
B. select只取最快完成的那一个
C. CountDownLatch等所有完成
D. select能超时返回
【答案】A、B、C、D
【解析】select和CountDownLatch都能多路监听,但select是先到先得,CountDownLatch是全部完成。
6. Channel实现CountDownLatch时,必须要做哪些事情?
A. 设置容量=计数总数
B. N个子协程各send一次
C. 主协程循环receive N次
D. receive时一定要用withTimeout防死锁
【答案】A、B、C
【解析】A/B/C是基本套路,D不是必须,只有担心卡死才用超时。
三、判断题(共6题)
1. Java的join、Future.get和协程的join、await都是阻塞当前线程的操作。
【答案】×
【解析】Java的是线程阻塞,协程的是挂起当前协程,非线程阻塞。
2. select可以实现“哪个任务先完成用哪个结果”,属于“竞速模式”。
【答案】√
【解析】select就是多路竞速,先到先得。
3. 协程用Job.join和async.await都能实现父协程等待子协程结果。
【答案】√
【解析】Job.join等待子任务,async.await还可返回结果。
4. Channel只能用作数据传递,无法做并发计数。
【答案】×
【解析】channel通过send/receive完全能模拟CountDownLatch的计数功能。
5. Java的CompletableFuture和Kotlin的Deferred没有任何相似之处。
【答案】×
【解析】都可代表异步结果,都有回调链和get/await功能。
6. select{}块里所有事件只能写一个onXXX,否则报错。
【答案】×
【解析】select可以同时监听多个事件,channel的onReceive/onSend只能选一个。
四、简答题(共4题)
1. 简述CountDownLatch与协程中用Channel实现“多个协程全部完成再继续”的等价写法。
【答案】
CountDownLatch本质是主线程await计数器变0。协程里用Channel容量设为N,N个子任务各send一次,主协程receive N次。所有子任务完成后主协程才能继续,完全等价。
2. select与Job.join、Deferred.await相比,有什么区别和优势?
【答案】
join/await是等待特定一个/全部任务完成,select可同时监听多个操作,哪个最先完成用哪个(竞速、先到先得)。优势是可以快速响应最快的异步结果,适合高并发/请求竞速场景。
3. 简述Java Future和CompletableFuture的主要区别。
【答案】
Future只能get阻塞式获取结果,功能单一。CompletableFuture支持非阻塞回调、链式操作、组合多个异步任务,更适合现代异步流式编程。
4. select块中onTimeout的意义是什么?
【答案】
onTimeout可设置select整体的超时处理,如果所有监听项都未完成,超时回调被触发(可返回默认值/报错/做降级),保证select不会永久挂起。
五、编程题(共3题)
1. Java:用CountDownLatch让主线程等待3个子线程全部完成后再继续。
【答案:】
CountDownLatch latch = new CountDownLatch(3);
for (int i = 0; i < 3; i++) {
new Thread(() -> {
// 子任务
Thread.sleep(1000);
latch.countDown();
}).start();
}
latch.await();
System.out.println("All done");
【解析】主线程await,3个子任务各countDown。
2. Kotlin:用async/await让主协程等待3个任务全部返回结果,并打印总和。
【答案:】
val d1 = async { delay(500); 1 }
val d2 = async { delay(1000); 2 }
val d3 = async { delay(700); 3 }
val sum = d1.await() + d2.await() + d3.await()
println("sum=$sum")
【解析】async并发,await等待所有返回结果相加。
3. Kotlin:用select监听2个channel,哪个先收到就打印其内容,超时1秒打印Timeout。
【答案:】
val ch1 = Channel<String>()
val ch2 = Channel<String>()
launch { delay(1500); ch2.send("b") }
launch { delay(500); ch1.send("a") }
val result = select<String> {
ch1.onReceive { "from1: $it" }
ch2.onReceive { "from2: $it" }
onTimeout(1000) { "Timeout" }
}
println(result)
【解析】ch1会先发,1秒内没发则打印Timeout。