
本文详解如何在多线程分治求最大值场景中,通过合理使用join()和线程安全设计(而非错误地同步run()方法)确保主线程正确等待子线程完成并安全获取结果。
本文详解如何在多线程分治求最大值场景中,通过合理使用join()和线程安全设计(而非错误地同步run()方法)确保主线程正确等待子线程完成并安全获取结果。
在Java并发编程中,一个常见误区是:当主线程需要汇总多个子线程的计算结果时,试图通过synchronized修饰run()或getMax()方法来“解决竞态”,但这不仅逻辑错误,更会严重损害并发性。问题本质并非数据竞争(max字段本身无跨线程读写冲突),而是线程执行顺序未受控导致的时序错误——主线程在子线程尚未完成run()前就调用了getMax(),读取到未初始化的默认值0。
✅ 正确解法:用 join() 显式等待线程终止
Thread.join() 是专为解决此类“主线程依赖子线程完成”问题而设计的标准机制。它阻塞当前线程,直到目标线程自然结束,无需加锁,语义清晰且高效。
以下是修复后的完整示例(关键修改已高亮):
public class MaxTask extends Thread {
private final int[] arr; // 建议final,避免意外修改
private int max;
private final int first, last;
public MaxTask(int[] arr, int first, int last) {
this.arr = arr;
this.first = first;
this.last = last;
}
public int getMax() {
return max; // 无需synchronized:仅在join后调用,此时max已稳定
}
@Override
public void run() { // 不应synchronized!否则所有任务串行执行,失去多线程意义
if (first > last || arr.length == 0) return;
max = arr[first];
for (int i = first + 1; i <= last; i++) {
if (arr[i] > max) max = arr[i];
}
}
}public class MainMax {
public static void main(String[] args) throws Exception {
int size = 100;
int workers = 10;
int[] arr = new int[size];
// 初始化数组...
for (int i = 0; i < size; i++) {
arr[i] = (int)(Math.random() * 100);
}
int gsize = (arr.length - 1) / workers;
MaxTask[] tasks = new MaxTask[workers];
// 启动所有子线程
int first = 0;
for (int i = 0; i < workers; i++) {
int last = Math.min(first + gsize, arr.length - 1); // 修正边界:避免越界
tasks[i] = new MaxTask(arr, first, last);
tasks[i].start();
first = last + 1;
}
// ✅ 关键步骤:主线程等待所有子线程完成
for (MaxTask task : tasks) {
task.join(); // 阻塞直到task.run()执行完毕
}
// 此时所有max均已计算完成,可安全读取
int maxmax = tasks[0].getMax();
for (int i = 1; i < tasks.length; i++) {
int temp = tasks[i].getMax();
if (temp > maxmax) maxmax = temp;
}
System.out.println("maxmax=" + maxmax);
}
}⚠️ 重要注意事项
- 不要同步 run() 方法:synchronized void run() 会使所有 MaxTask 实例串行执行,彻底抵消多线程优势,性能可能比单线程更差。
- getMax() 无需同步:因 join() 保证了调用 getMax() 时 run() 已结束,max 字段状态已确定,不存在可见性问题(join() 具有内存屏障语义,确保之前写入对后续读取可见)。
- 边界处理要严谨:原代码中 last = first + gsize 可能导致索引越界(如 arr.length=100, workers=10 → gsize=9, 最后一组 last=99 合理;但若 size=101 则需 Math.min(...) 防御)。
- 现代替代方案推荐:生产环境建议使用 ExecutorService + Future 或 ForkJoinPool,它们提供更健壮的生命周期管理、异常处理和资源复用能力。
总结
解决“主线程过早读取子线程结果”的核心在于控制执行时序,而非引入不必要的同步。Thread.join() 是轻量、标准且语义明确的解决方案。理解 join() 的阻塞行为与内存可见性保证,是编写可靠多线程程序的基础。盲目添加 synchronized 不仅不能解决问题,反而会掩盖真实缺陷并引入性能瓶颈。
立即学习“Java免费学习笔记(深入)”;










