CodeGym /课程 /JAVA 25 SELF /并行编程最佳实践

并行编程最佳实践

JAVA 25 SELF
第 54 级 , 课程 4
可用

1. 能并行的不一定都该并行

在 Java 中有很多并行任务的实现方式。但“并行 = 一定更快”的想法,就像往汤里一直加盐就会更好喝一样:在某个点之前可能是的,再往后——最好别试。

ExecutorService 非常适合有明确生命周期、需要启动与控制的任务:请求处理、异步数据加载、彼此独立的计算。你可以自己决定线程池的大小,并管理任务的生命周期。

parallelStream 是在操作彼此独立且无副作用的集合时实现并行的快捷方式。对于“重”的集合(几十万元素及以上)才更划算。

ForkJoinPool 适用于易于拆分为子任务的场景(divide & conquer):排序、搜索、大数组聚合。它被 parallelStream 内部使用,但也可以直接控制。

不要“以防万一”就使用并行。如果任务很小,调度、上下文切换和同步的开销可能会吞掉所有收益。

示例:什么时候不需要并行

List<Integer> smallList = List.of(1, 2, 3, 4, 5);
int sum = smallList.parallelStream()
    .mapToInt(x -> x)
    .sum(); // 为了 5 个数字而并行——大材小用!

2. 线程安全:避免共享可变数据

在并行世界里,最大的威胁是数据竞争(race conditions)。当多个线程修改同一个变量时,结果可能超出预期。

  • 避免共享的可变变量。 即便像 counter++ 这样的表达式也不是原子操作。
  • 使用线程安全的集合与原子操作。 例如,ConcurrentHashMapCopyOnWriteArrayListAtomicIntegerAtomicLong
  • 在并行流中避免副作用。 不要在 parallelStream 中修改外部结构。

反例

List<Integer> numbers = Arrays.asList(1,2,3,4,5);
List<Integer> result = new ArrayList<>();
numbers.parallelStream().forEach(n -> result.add(n * 2)); // 危险!

这里的 result.add() 不是线程安全的。结果可能是丢失元素或抛出异常。

正确做法?

List<Integer> result = numbers.parallelStream()
    .map(n -> n * 2)
    .collect(Collectors.toList());

3. 性能:线程更多并不总是更好

小任务并行不划算。 如果工作仅耗时毫秒级,并行启动往往会因为开销而使执行更慢。

要测量性能。 进行快速测量可使用 System.nanoTime()

long start = System.nanoTime();
// ... 你的代码 ...
long end = System.nanoTime();
System.out.println("Execution time: " + (end - start) + " 纳秒");

进行严肃的微基准测试请使用 JMH(Java Microbenchmark Harness)。

示例:比较顺序流与并行流

List<Integer> bigList = IntStream.range(0, 1_000_000)
    .boxed().collect(Collectors.toList());

long t1 = System.nanoTime();
long sum1 = bigList.stream().mapToLong(x -> x).sum();
long t2 = System.nanoTime();
long sum2 = bigList.parallelStream().mapToLong(x -> x).sum();
long t3 = System.nanoTime();

System.out.println("Sequential: " + (t2 - t1) / 1_000_000 + " 毫秒");
System.out.println("Parallel:     " + (t3 - t2) / 1_000_000 + " 毫秒");

在自己的电脑上试试——对确实很大的集合和“重”操作,收益会更加明显。

4. 错误处理:不要忽略线程中的异常

Future 与异常处理

如果通过 ExecutorService.submit() 启动任务,异常并不会自动“冒泡”——需要通过 Future.get() 来处理:

Future<Integer> future = executor.submit(() -> {
    if (Math.random() > 0.5) throw new RuntimeException("哎呀!");
    return 42;
});
try {
    Integer result = future.get(); // 可能抛出 ExecutionException
} catch (ExecutionException e) {
    System.err.println("任务中的错误: " + e.getCause());
}

ForkJoin 与异常处理

ForkJoinPool 中,异常会被“封装”在任务里。调用 join()/get() 时它们会抛出:

ForkJoinPool pool = new ForkJoinPool();
RecursiveTask<Integer> task = new MyTask();
try {
    int result = pool.invoke(task);
} catch (Exception e) {
    System.err.println("ForkJoin 中的错误: " + e);
}

不要忘记处理 InterruptedException

很多方法(例如 Future.get()Thread.sleep())可能抛出 InterruptedException。不要“吞掉”它——要正确响应:设置中断标志或结束任务。

5. 并行代码的调试与测试

日志与调试

并发 bug 难以捉摸,且经常不稳定地出现。记录包含线程的信息:Thread.currentThread().getName()。这有助于弄清是谁在何时执行了代码。

在复杂情况下,使用支持多线程的调试器(例如 IntelliJ IDEA)。临时的 Thread.sleep() 有时可以帮助“捕捉”罕见的数据竞争。

多线程场景的测试

为并行操作编写独立测试,并使用条件等待工具,例如 Awaitility。多次运行这些测试:有些问题只有在第 100 次或第 1000 次运行时才会暴露。

6. 实用细节与建议

可读性与可维护性:写清晰的并行代码

  • 要有文档。 对复杂部分和工具选择进行注释与说明。
  • 使用高层抽象。 比起手动管理线程,更推荐 ExecutorServiceparallelStreamForkJoinPool
  • 避免“魔法”。 如果可以更简单,就不要把同步搞得过于复杂。

表格:在什么场景使用哪种工具

场景 推荐工具
大量相互独立的任务
ExecutorService
处理大型集合 parallelStreamForkJoin
“分而治之”的任务
ForkJoinPool + RecursiveTask
简单的异步任务
CompletableFuture
大量小任务 顺序流
带有副作用的任务 只能使用线程安全的集合!

并行程序员的“守则”

  • 不要使用共享可变变量——除非确定它们是线程安全的。
  • 不要为并行而并行:先评估潜在收益。
  • 不要忘记关闭线程池:shutdown()/shutdownNow()
  • 不要把 parallelStream 用于带副作用的操作。
  • 不要忘记处理来自 FutureForkJoinTask 的异常。
  • 不要“吞掉” InterruptedException——要正确结束任务。

7. 并行编程中的常见错误

错误 1:对小任务进行并行化。 新手常常把一切都并行化,哪怕工作仅是微秒级。结果——由于开销更慢。

错误 2:在流中引入副作用。parallelStream 中不能修改外部变量或集合——会引发数据竞争和不可预测的 bug。

错误 3:忽略异常。 如果不处理来自 Future.get()ForkJoinTask 的错误,你就不知道任务为何失败。

错误 4:忘记对 ExecutorService 调用 shutdown() 没有显式关闭,应用在退出时可能“挂住”。

错误 5:使用非线程安全的集合。 在多个线程中往普通 ArrayList 写入——是直奔错误之路。

错误 6:“吞掉” InterruptedException 如果线程被中断——请尊重它并正确结束工作。

错误 7:过度复杂的同步逻辑。 过多的 synchronized 块会导致 死锁/活锁。优先选择高层抽象。

1
调查/小测验
并行和ForkJoin第 54 级,课程 4
不可用
并行和ForkJoin
并行和ForkJoin
评论
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION