join()是最直接实现线程顺序执行的方式,主线程依次启动t1、t2并调用其join()等待结束;CountDownLatch适用于事件驱动的时序控制,如等待多个线程完成后再启动下一个;synchronized和ReentrantLock仅保证临界区互斥,不控制线程执行顺序;Thread.sleep()因不可靠、不精确而应避免用于顺序控制。

用 join() 实现线程顺序执行最直接
如果只是让几个线程按固定顺序一个接一个跑完(比如 t1 → t2 → t3),join() 是最轻量、最直观的方式。它会让当前线程阻塞,直到被调用 join() 的线程终止。
-
t1.start()后立刻调用t1.join(),主线程就卡在这儿等t1结束 - 再
t2.start()+t2.join(),依此类推 - 注意:不能在
t1.run()里调t2.join()—— 那是在t1线程里等t2,而t2可能根本没 start,会永久阻塞 - 所有
join()调用必须在启动线程之后、且由同一线程(通常是主线程)发起
Thread t1 = new Thread(() -> System.out.println("t1 done"));
Thread t2 = new Thread(() -> System.out.println("t2 done"));
t1.start();
try { t1.join(); } catch (InterruptedException e) { Thread.currentThread().interrupt(); }
t2.start();
try { t2.join(); } catch (InterruptedException e) { Thread.currentThread().interrupt(); }
用 CountDownLatch 控制多个线程的启动/结束时序
当需要“等前 N 个线程都完成,再启动第 N+1 个”,或者“多个线程都等同一个信号才开始”,CountDownLatch 比 join() 更灵活。它的核心是计数器,countDown() 减一,await() 阻塞直到归零。
- 初始化时传入计数值,比如
new CountDownLatch(2) - 前两个线程执行完关键逻辑后各自调一次
latch.countDown() - 第三个线程一开始先
latch.await(),只有等够两次countDown()才继续 - 和
join()不同:它不关心线程对象生命周期,只关心事件发生次数;也支持超时await(3, TimeUnit.SECONDS)避免死等 - 计数器不可重置,用完即弃;需要循环控制请换
CyclicBarrier
用 synchronized 或 ReentrantLock 保证临界区串行,但不等于线程执行顺序
很多人误以为加锁就能控制线程“谁先谁后运行”,其实不然。synchronized 和 ReentrantLock 只保证同一时刻只有一个线程进入临界区,但哪个线程抢到锁是不确定的——JVM 和 OS 调度决定,不是代码书写顺序。
- 即使你按
t1、t2、t3顺序start(),也不代表它们按此顺序获得锁 - 若真要强制顺序(如必须
t1先改数据、t2再读),得配合状态变量 +wait()/notify()或Condition,复杂度陡增 - 单纯加锁适合保护共享资源,不适合编排执行流程;强行用锁模拟顺序容易写出活锁或响应延迟高的代码
为什么不用 Thread.sleep() 控制顺序
靠 sleep() “估算”时间来错开线程执行,是典型反模式。它既不精确也不可靠。
立即学习“Java免费学习笔记(深入)”;
- CPU 负载高时,
sleep(100)可能实际挂起 200ms 甚至更久 - 不同机器、JVM 版本、GC 活动都会影响调度精度
- 无法感知目标线程是否真的完成了工作 —— 它可能刚 sleep 就抛异常退出了
- 一旦逻辑变复杂(比如某个步骤耗时突增),整个时序就崩,debug 成本远高于用同步原语
真正需要顺序执行的地方,几乎总是存在明确的“完成信号”或“前置依赖”,应该用 join()、CountDownLatch、CompletableFuture.thenRun() 这类有语义的机制去表达,而不是靠时间硬等。










