
`join()` 仅保证主线程等待指定线程结束,但不解决多线程并发修改共享变量导致的数据竞争问题;若未同步访问,即使正确调用 `join()`,结果仍不可预测。
在 Java 多线程编程中,Thread.join() 常被误认为是“让线程串行执行”的银弹。实际上,它的语义非常明确:当前线程(如主线程)阻塞,直到被调用 join() 的目标线程完全终止。它不控制目标线程何时启动、不干预其内部逻辑、更不保证共享资源的线程安全性。上文示例中出现的随机输出(如 14125, 14125),根本原因并非 join() 失效,而是两个线程同时对非原子、未同步的 volatile int count 进行自增操作,引发了经典的数据竞争(Data Race)。
? 为什么 volatile 无法修复这个问题?
volatile 仅保证变量的可见性(一个线程修改后,其他线程能立即看到最新值)和禁止指令重排序,但它不保证操作的原子性。count++ 在字节码层面等价于三步:
int temp = count; // 读取 temp = temp + 1; // 计算 count = temp; // 写回
当 t1 和 t2 并发执行这三步时,完全可能出现如下竞态场景:
- t1 读取 count=100 → t2 也读取 count=100
- t1 计算 101 → t2 也计算 101
- t1 写回 101 → t2 写回 101(覆盖!)
最终 count 仅增加 1 次,而非预期的 2 次。这就是典型的 丢失更新(Lost Update)。
✅ 正确方案:分离“等待控制”与“数据保护”
要获得确定性结果(如 10000 → 20000),必须同时满足两个条件:
立即学习“Java免费学习笔记(深入)”;
- 执行顺序可控(通过 join() 或其他机制)
- 共享状态访问线程安全(通过同步机制)
方案一:串行化执行(最简单,适合理解原理)
public void execute() {
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++ 的原子性:
private int count = 0;
private final Object lock = new Object(); // 或使用 AtomicInteger
// 替换 run() 中的循环为:
synchronized (lock) {
count++;
}
// 或更高效:new AtomicInteger().incrementAndGet()完整安全版本(使用 AtomicInteger):
import java.util.concurrent.atomic.AtomicInteger;
public class TestJoinMethod {
private final AtomicInteger count = new AtomicInteger(0);
public void execute() {
Thread t1 = new Thread(() -> {
for (int i = 0; i < 10000; i++) {
count.incrementAndGet(); // 原子操作
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 10000; i++) {
count.incrementAndGet();
}
});
t1.start();
t2.start();
try {
t1.join(); // 等待 t1 结束(此时 t2 可能仍在运行)
System.out.println("After t1 join: " + count.get()); // 如 12345(不确定)
t2.join(); // 等待 t2 结束
System.out.println("After both joins: " + count.get()); // 确定为 20000
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}⚠️ 关键注意事项
- join() 不等于同步:它只影响线程生命周期的等待关系,与内存可见性、原子性无关。
- volatile ≠ 线程安全:仅适用于“纯读写”场景(如标志位),不适用于复合操作(++, +=, check-then-act)。
- 避免过早优化:若业务逻辑天然串行,强行多线程反而增加复杂度和开销。
- 优先使用高级并发工具:AtomicInteger、ReentrantLock、CountDownLatch 等比手动 synchronized 更简洁可靠。
✨ 总结
join() 是线程协作的基石,但绝非并发安全的万能解药。真正的线程安全需要从设计层面识别共享状态,并选用恰当的同步机制(原子类、锁、不可变对象等)加以保护。理解“等待”与“保护”的分离,是写出健壮多线程代码的第一步。









