
本文详解为何仅对getMax()和run()加synchronized看似“修复”了竞态问题,实则掩盖了根本缺陷;重点讲解如何用join()保证主线程等待子线程完成,并指出错误同步策略的危害与更优替代方案。
本文详解为何仅对`getmax()`和`run()`加`synchronized`看似“修复”了竞态问题,实则掩盖了根本缺陷;重点讲解如何用`join()`保证主线程等待子线程完成,并指出错误同步策略的危害与更优替代方案。
在您提供的代码中,主线程在启动所有 MaxTask 子线程后,立即调用 getMax() 获取结果,而此时子线程很可能尚未执行完 run() 中的循环计算——这导致读取到未初始化或过时的 max 值(如默认值 0),造成结果错误。这是一个典型的线程间可见性与执行顺序问题。
❌ 错误解法:同步 run() 和 getMax() 的本质误区
您提到“加 synchronized 后能工作”,但需明确:
- 若为 run() 方法添加 synchronized(即 public synchronized void run()),意味着每个线程必须串行执行——同一时刻仅有一个 MaxTask 在运行,完全丧失并行性,10 个线程退化为单线程遍历,性能反降;
- 若仅为 getMax() 添加 synchronized(即 public synchronized int getMax()),虽能保证读操作的原子性,但无法解决可见性问题:主线程仍可能在子线程写入 max 之前就读取,因为 JVM 不保证非同步写操作对其他线程的及时可见。
✅ 正确思路不是“锁住执行”,而是显式协调线程生命周期:让主线程主动等待所有子线程完成后再读取结果。
✅ 推荐解法:使用 join() 确保执行顺序
Thread.join() 是专为此类场景设计的标准机制:它阻塞当前线程,直到目标线程终止。修改 MainMax 主方法如下:
立即学习“Java免费学习笔记(深入)”;
// ... 启动线程部分保持不变
for (int i = 0; i < workers; i++) {
last = first + gsize;
tasks[i] = new MaxTask(arr, first, last);
tasks[i].start();
first = last + 1;
}
// ✅ 关键:等待所有子线程完成
for (int i = 0; i < workers; i++) {
tasks[i].join(); // 主线程在此处暂停,直到 tasks[i] 执行完毕
}
// ✅ 此时所有 max 值已安全写入,可安全读取
int maxmax = tasks[0].getMax();
for (int i = 1; i < workers; i++) {
int temp = tasks[i].getMax();
if (temp > maxmax) maxmax = temp;
}
System.out.println("maxmax=" + maxmax);? 进阶建议:避免共享可变状态,拥抱不可变/线程安全设计
当前 MaxTask 类通过共享字段 max 传递结果,易引发并发风险。更健壮的设计是:
- 移除 max 字段与 getMax(),改为 run() 计算后直接存储到线程安全容器中;
- 或让 MaxTask 实现 Callable
,配合 ExecutorService 统一管理:
ExecutorService executor = Executors.newFixedThreadPool(workers);
List<Future<Integer>> futures = new ArrayList<>();
for (int i = 0; i < workers; i++) {
futures.add(executor.submit(new MaxCallable(arr, first, last)));
first = last + 1;
}
int maxmax = futures.get(0).get(); // 自动阻塞等待结果
for (Future<Integer> future : futures) {
maxmax = Math.max(maxmax, future.get()); // get() 阻塞直至完成
}
executor.shutdown();⚠️ 注意事项总结
- 勿滥用 synchronized 于 run():违背多线程初衷,引入不必要串行瓶颈;
- join() 是轻量级、语义清晰的等待机制,无锁开销,推荐优先使用;
- 避免过度创建线程:workers = 10 对小数组(size=100)得不偿失,应根据 CPU 核心数与任务粒度合理设置;
- volatile 不适用此场景:max 是普通写入,volatile 仅保证可见性,不保证 run() 执行完成,仍需 join() 或其他同步手段。
通过 join() 显式编排线程执行顺序,既保证了结果正确性,又保留了并行计算的性能优势——这才是 Java 并发编程中“控制流”与“数据流”分离的优雅实践。










