
本文深入解析`join()`方法的作用边界:它仅保证主线程等待目标线程结束,但无法解决多线程并发修改共享变量导致的数据竞争问题;要获得确定性结果,必须配合同步机制或重构执行顺序。
在多线程编程中,Thread.join() 常被误认为是“让线程串行执行”的银弹。实际上,它的职责非常明确:使当前线程阻塞,直到被调用 join() 的线程完全终止。它不控制线程启动时机,不保证内存可见性,更不提供任何互斥保护——这些常见误解,正是示例代码输出非预期值(如 14125, 14125)的根本原因。
我们来逐步拆解原始代码的问题:
? 问题根源分析
t1.start(); // ✅ 启动 t1
t2.start(); // ✅ 启动 t2 → 此刻 t1 和 t2 已**并发运行**
try {
t1.join(); // ⚠️ 等待 t1 结束,但 t2 早已在运行中!
System.out.println(count); // ❌ 此时 count 是 t1 + 部分/全部 t2 的混合结果
t2.join(); // ⚠️ 再等 t2 结束(可能已结束)
System.out.println(count); // ❌ 两次打印几乎相同,因 t2 很可能早已完成
} catch (InterruptedException e) { /* ... */ }关键误区有两点:
- join() 不等于“顺序启动”:t1.start() 和 t2.start() 紧密调用,不代表 t2 会等 t1 完成才开始;它们几乎同时进入就绪态,由调度器决定执行顺序。
- volatile 无法解决原子性问题:count++ 是典型的“读-改-写”三步操作(读取 count → 加 1 → 写回),volatile 仅保证可见性和禁止重排序,不保证该操作的原子性。两个线程同时执行 count++,必然发生丢失更新(Lost Update)。
因此,最终 count 值远小于预期的 20000(10000 + 10000),且每次运行结果不同——这是典型的数据竞争(Data Race)表现。
立即学习“Java免费学习笔记(深入)”;
✅ 正确解决方案
方案一:逻辑串行化(放弃并发,确保顺序)
若业务本质要求“先执行 t1 再执行 t2”,则无需多线程:
public void executeSequential() {
Thread t1 = new Thread(() -> {
for (int i = 0; i < 10000; i++) count++;
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 10000; i++) count++;
});
t1.start();
try {
t1.join(); // 等 t1 完全结束
System.out.println("After t1: " + count); // 输出 10000
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return;
}
t2.start();
try {
t2.join(); // 等 t2 完全结束
System.out.println("After t2: " + count); // 输出 20000
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}✅ 优点:结果绝对可预测,代码简洁。 ⚠️ 缺点:无并发收益,性能等同单线程。
方案二:真正并发 + 线程安全(推荐)
若需利用多核并行提升性能,必须保障对 count 的安全访问:
public class TestJoinMethod {
private int count = 0; // 移除 volatile —— 此处它无意义
private final Object lock = new Object();
public void executeConcurrentSafe() {
Thread t1 = new Thread(() -> {
for (int i = 0; i < 10000; i++) {
synchronized (lock) {
count++; // ✅ 原子性由 synchronized 保证
}
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 10000; i++) {
synchronized (lock) {
count++;
}
}
});
t1.start();
t2.start();
try {
t1.join(); // 主线程等待 t1 结束
t2.join(); // 主线程等待 t2 结束
System.out.println("Final count: " + count); // ✅ 稳定输出 20000
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}其他安全替代方案包括:
- 使用 AtomicInteger(推荐,无锁高性能):
private AtomicInteger count = new AtomicInteger(0); // 替换 count++ 为 count.incrementAndGet();
- 使用 ReentrantLock(更灵活的锁控制);
- 使用 LongAdder(高并发累加场景更优)。
? 关键总结与注意事项
- join() 的唯一职责是线程生命周期同步,它既不控制线程启动顺序,也不提供内存同步或互斥能力。
- volatile ≠ 线程安全:它只解决可见性与有序性,对复合操作(如 ++, +=, list.add())完全无效。
- 不要为了用线程而用线程:若任务天然串行,强行多线程反而增加复杂度和开销。
- 始终问自己:共享数据是否会被多个线程同时读写?若是,必须使用同步机制(synchronized, Lock, Atomic*, Concurrent* 等)。
- 调试技巧:在开发阶段,可临时将循环次数设为小值(如 i
掌握 join() 的真实语义,并结合正确的并发工具,才能写出既高效又可靠的多线程代码。










