join()是最直接轻量的线程顺序控制方式,需在start()后调用;CountDownLatch适用于多线程并行后汇合,依赖计数器归零;CyclicBarrier支持重复使用的循环同步,适合分阶段协作;避免用锁控制执行顺序。

用 join() 强制等待线程结束
当需要「A 线程执行完,再执行 B 线程」这种明确先后关系时,join() 是最直接、最轻量的选择。它会让当前线程阻塞,直到目标线程终止。
常见错误是调用顺序写反:比如在主线程中启动 t1 后立刻 t2.join(),结果等的是还没 start 的 t2,导致主线程卡死或逻辑错乱。
- 必须在
t.start()之后调用t.join() - 如果多个线程需串行,按执行顺序依次
join():先t1.join(),再t2.join() -
join(long millis)可设超时,避免无限等待;超时后线程可能仍在运行,需配合isAlive()判断
Thread t1 = new Thread(() -> System.out.println("t1 done"));
Thread t2 = new Thread(() -> System.out.println("t2 done"));
t1.start();
t1.join(); // 主线程等 t1 结束
t2.start();
t2.join(); // 再等 t2 结束
用 CountDownLatch 控制多线程汇合点
适用于「多个线程并行干活,但主线程必须等它们全做完才能继续」的场景。和 join() 不同,它不关心线程生命周期,只认计数器归零。
容易踩的坑是初始化值与 countDown() 调用次数不匹配:少调一次,主线程永远阻塞;多调一次,可能提前放行,引发竞态。
立即学习“Java免费学习笔记(深入)”;
- 构造时传入预期线程数:
new CountDownLatch(3) - 每个工作线程完成时调用一次
countDown()(通常放在finally块里) - 主线程调用
await()阻塞,或await(long, TimeUnit)设超时 - 计数器为 0 后,所有等待线程被唤醒,且该 latch 不可重用
用 CyclicBarrier 实现多线程循环同步点
和 CountDownLatch 类似,但支持重复使用,适合分阶段协作任务,比如「每轮 4 个线程各自计算一部分,全部到达后合并结果,再进下一轮」。
注意 CyclicBarrier 的「屏障动作」(Runnable 参数)是在最后一个线程到达时、其他线程被唤醒前执行的,常用来做汇总操作。若此处抛异常,所有等待线程会收到 BrokenBarrierException。
- 初始化时指定参与线程数:
new CyclicBarrier(4) - 每个线程调用
await()进入等待,到达后自动释放全部线程 - 可选传入
Runnable作为屏障触发时的回调(仅由最后一个到达的线程执行) - 调用
reset()可强行重置,但正在等待的线程会收到BrokenBarrierException
别用 synchronized 或 ReentrantLock 控制执行顺序
锁解决的是「临界资源互斥访问」,不是「线程执行先后」。试图靠加锁让线程 A 必须先于线程 B 执行,本质是设计错误——锁不提供调度语义,JVM 和 OS 调度器仍可能任意切换线程。
典型误用:两个线程分别持有不同锁,却指望它们按代码书写顺序执行。结果往往是不可预测的交替或死锁。
-
synchronized块只保证同一时刻最多一个线程进入,不控制谁先进、谁后进 - 即使加了
wait()/notify(),也需配合条件变量和循环等待,复杂度陡增,且易出错 - 真正需要顺序控制时,优先选
join()、CountDownLatch、CyclicBarrier这类语义明确的工具
线程执行顺序本身是脆弱的依赖,越想强行固化,越容易在高并发、多核、JIT 优化等环境下失效。重点应放在解耦任务、定义清晰的同步点,而不是纠结「谁必须在谁前面跑」。










