ForkJoinPool 使用的错误写法

本文分析了在使用ForkJoinPool时,错误地依次调用fork()和join()会导致线程资源浪费和过多线程创建的问题。正确的做法是使用invokeAll()并行执行任务,以提高效率并避免线程等待。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

compute分出多个task后:以task1,task2为例

错误做法:

1)依次执行task1.fork(),task2.fork()

2)依次执行task1.join(),task2.join()

正确做法:

1)直接调用invokeAll(task1,task2)


fork():

把task置入当前ForkJoinWorkerThread的queue中,等待被消费

join():

当前ForkJoinWorkerThread执行等待该task执行结束


错误做法过程:

将task1与task2置入queue中,当前ForkJoinWorkerThread停下来等待task1,task2的执行结束。

分析:

当前的ForkJoinWorkerThread可以说join之后什么事情都没有做,只是等待ing。而task1和task2会在新的线程中执行。

会浪费当前ForkJoinWorkerThread的宝贵线程资源。而且最糟糕的是,会创建一个新的ForkJoinWorkerThread去执行新的task,可想而知,如果任务量特别多,那会同时开启特别多的线程。

以上一篇提到的count来看,

打开VisualVM,可以看到这里已经创建太多的守护线程,而且每个线程都处于等待状态。



正确做法过程:

将task1置入queue中,当前ForkJoinWorkerThread执行task2,最后停下来等待task1的执行结束。

分析:

当前ForkJoinWorkerThread一直在运行,把需要执行task都直接执行。

充分利用的线程资源,当前ForkJoinWorkerThread也会执行一个task的任务,不会让当前ForkJoinWorkerThread傻等结果。



package spliteratorDemo; import java.util.List; import java.util.Spliterator; import java.util.concurrent.ForkJoinPool; import java.util.concurrent.RecursiveTask; import java.util.stream.Collectors; import java.util.stream.IntStream; /** * 处理100万个整数集 计算每个元素的平方值并求和 */ public class SpliteratorDemo { public static void main(String[] args) { /** * IntStream.rangeClosed(1, 1_000_000) * * IntStream是Java 8引入的用于处理原始int类型元素的流。 * * rangeClosed(int startInclusive, int endInclusive)是IntStream的一个静态方法,它生成一个从startInclusive到endInclusive(包含)的顺序有序整数流。这里生成的是从1到1,000,000(包含)的整数流。 * * .boxed() * * boxed()方法将IntStream(原始int值的流)转换为Stream<Integer>(包装类型的流)。这是因为原始类型的流不能直接收集到对象集合(如List)中,需要将每个int值装箱成Integer对象。 * * .collect(Collectors.toList()) * * collect是流操作的一个终端操作,它将流中的元素累积到一个可变的结果容器中。 * * Collectors.toList()是一个收集器,它将流中的元素收集到一个新的List中。这里收集的是Stream<Integer>中的元素,因此最终得到一个List<Integer>。 * * 因此,整行代码的作用是:创建一个包含从1到1,000,000(包含)的整数的List。 */ List<Integer> numbeers = IntStream.rangeClosed(1, 1_000_000).boxed().collect(Collectors.toList()); Spliterator<Integer> spliterator = numbeers.spliterator(); ForkJoinPool pool = ForkJoinPool.commonPool(); long startTimeParrel = System.currentTimeMillis(); long result = pool.invoke(new ComputeTask(spliterator)); long endTimeParrel = System.currentTimeMillis(); long durationParrel = endTimeParrel - startTimeParrel; System.out.println("并行计算结果:" + result+"耗时:"+durationParrel+"毫秒"); long startTimeSingle = System.currentTimeMillis(); long expect = numbeers.stream().mapToLong(i -> i * i).sum(); long endTimeSingle = System.currentTimeMillis(); long durationSingle = endTimeSingle - startTimeSingle; System.out.println("单线程计算结果" + expect+"耗时:"+durationSingle+"毫秒"); System.out.println("结果验证"+(result==expect?"正确":"错误")); } static class ComputeTask extends RecursiveTask<Long> { private final Spliterator<Integer> spliterator; private static final int THRESHOLD = 100_000; public ComputeTask(Spliterator<Integer> spliterator) { this.spliterator = spliterator; } @Override protected Long compute() { if (spliterator.estimateSize() <= THRESHOLD) { return computeSequentially(); } Spliterator<Integer> split = spliterator.trySplit(); if (split == null) { return computeSequentially(); } ComputeTask leftTask = new ComputeTask(split); ComputeTask rightTask = new ComputeTask(spliterator); leftTask.fork(); return rightTask.compute() + leftTask.join(); } private long computeSequentially() { long sum = 0; spliterator.forEachRemaining(num -> sum += num * num); return sum; } } } 请问以上的代码中有什么问题
最新发布
07-11
<think>我们正在讨论如何在Java中使用ForkJoinPool嵌套线程池。嵌套线程池指的是在一个ForkJoinPool的任务中再提交任务到另一个线程池(可能是另一个ForkJoinPool,也可能是其他类型的线程池,如ThreadPoolExecutor)。需要注意的是,在ForkJoinPool的任务中提交新任务到同一个ForkJoinPool可能会导致工作线程被阻塞(因为等待子任务完成),从而可能引起性能问题(如线程饥饿)。因此,通常建议避免在ForkJoinPool的任务中再提交任务到同一个ForkJoinPool,但可以提交到不同的线程池。 以下是一个示例,展示如何在ForkJoinPool的任务中使用另一个线程池(比如一个普通的ThreadPoolExecutor)来执行嵌套任务: 步骤: 1. 创建第一个ForkJoinPool(作为外层线程池)。 2. 在提交给外层ForkJoinPool的任务中,我们创建一个不同的线程池(例如,一个固定大小的ThreadPoolExecutor)来执行内部任务。 3. 注意:为了避免死锁和资源耗尽,要确保内部线程池的大小是有限的,并且不会过度占用系统资源。 示例代码: ```java import java.util.concurrent.*; public class NestedPoolsExample { public static void main(String[] args) throws Exception { // 外层ForkJoinPool ForkJoinPool forkJoinPool = new ForkJoinPool(); // 提交一个任务到外层ForkJoinPool forkJoinPool.submit(() -> { System.out.println("Outer task running in " + Thread.currentThread().getName()); // 创建一个内部线程池(这里使用固定大小的线程池) ExecutorService innerExecutor = Executors.newFixedThreadPool(2); // 提交一些任务到内部线程池 Future<?> future1 = innerExecutor.submit(() -> { System.out.println("Inner task 1 running in " + Thread.currentThread().getName()); }); Future<?> future2 = innerExecutor.submit(() -> { System.out.println("Inner task 2 running in " + Thread.currentThread().getName()); }); // 等待内部任务完成 try { future1.get(); future2.get(); } catch (InterruptedException | ExecutionException e) { e.printStackTrace(); } finally { innerExecutor.shutdown(); // 关闭内部线程池 } }).get(); // 等待外层任务完成 forkJoinPool.shutdown(); // 关闭外层线程池 } } ``` 注意:在这个例子中,外层任务在ForkJoinPool中执行,然后它创建了一个独立的内部线程池(ThreadPoolExecutor)来执行两个内部任务。外层任务等待内部任务完成后再结束。 但是,如果我们需要在ForkJoinPool的任务中再使用ForkJoinPool(即嵌套ForkJoinPool),则需要格外小心,因为ForkJoinPool的工作线程在等待子任务完成时可能会被阻塞,如果所有线程都在等待,就会导致死锁。因此,通常不建议嵌套使用同一个ForkJoinPool(尤其是默认的common池),但可以创建不同的ForkJoinPool实例。 示例:嵌套使用两个不同的ForkJoinPool实例 ```java import java.util.concurrent.*; public class NestedForkJoinPools { public static void main(String[] args) throws Exception { // 外层ForkJoinPool ForkJoinPool outerPool = new ForkJoinPool(); outerPool.submit(() -> { System.out.println("Outer task in " + Thread.currentThread().getName()); // 内层ForkJoinPool使用不同的实例) ForkJoinPool innerPool = new ForkJoinPool(); innerPool.submit(() -> { System.out.println("Inner task in " + Thread.currentThread().getName()); }).join(); // 等待内层任务完成 innerPool.shutdown(); // 关闭内层线程池 }).join(); // 等待外层任务完成 outerPool.shutdown(); } } ``` 注意:这里我们使用了两个独立的ForkJoinPool实例(outerPool和innerPool)。外层任务提交到outerPool,内层任务提交到innerPool。这样就不会因为outerPool的工作线程等待innerPool的任务而阻塞outerPool的所有线程(因为innerPool有自己的工作线程)。 然而,创建多个ForkJoinPool实例可能会消耗更多系统资源,因此需要根据实际情况权衡。 另外,在ForkJoinPool的任务中,我们也可以使用ForkJoinTask(如RecursiveAction)来分解子任务,这是ForkJoinPool的常规用法,并不属于嵌套线程池,而是同一个线程池内的任务分解。 总结: - 嵌套线程池通常指的是在一个线程池的任务中提交任务到另一个线程池。 - 可以使用ForkJoinPool和ThreadPoolExecutor的组合,也可以使用多个ForkJoinPool实例(避免使用同一个实例嵌套提交,以免死锁)。 - 需要谨慎管理线程池的生命周期,及时关闭。 引用说明: 在ForkJoinPool的任务中创建另一个线程池(如ThreadPoolExecutor)来执行内部任务,可以避免同一个ForkJoinPool内的嵌套问题[^1][^4]。而嵌套使用不同的ForkJoinPool实例也是可行的,但要注意资源管理[^4]。 根据以上内容,我们可以生成相关问题。 相关问题: 1. 在ForkJoinPool中嵌套使用同一个ForkJoinPool实例会有什么问题? 2. 如何避免嵌套线程池导致的死锁? 3. 除了ForkJoinPool,还有哪些线程池适合嵌套使用? 4. 嵌套线程池时如何管理线程池的生命周期?</think>### 在 Java 中使用 ForkJoinPool 嵌套线程池的方法 在 Java 中嵌套使用 `ForkJoinPool` 需要特别注意线程池的层次结构和任务分解策略,以避免死锁和资源竞争问题。以下是具体实现方法和示例: #### 核心实现方法 1. **分层线程池设计**: - 外层线程池处理粗粒度任务 - 内层线程池处理细粒度子任务 - 使用 `RecursiveTask`/`RecursiveAction` 实现任务分解 2. **避免共享线程池**: ```java // 创建独立的线程池实例 ForkJoinPool outerPool = new ForkJoinPool(4); // 外层池 ForkJoinPool innerPool = new ForkJoinPool(2); // 内层池 ``` 3. **任务提交策略**: ```java outerPool.submit(() -> { // 外层任务中提交到内层池 innerPool.submit(() -> { // 内层任务逻辑 }); }); ``` #### 完整示例代码 ```java import java.util.concurrent.*; public class NestedForkJoinDemo { // 内层任务类 static class InnerTask extends RecursiveTask<Integer> { private final int n; InnerTask(int n) { this.n = n; } @Override protected Integer compute() { if (n <= 1) return n; InnerTask f1 = new InnerTask(n - 1); f1.fork(); InnerTask f2 = new InnerTask(n - 2); return f2.compute() + f1.join(); // 递归计算[^5] } } // 外层任务类 static class OuterTask extends RecursiveTask<Integer> { private final int[] data; OuterTask(int[] data) { this.data = data; } @Override protected Integer compute() { if (data.length <= 2) { // 小任务直接计算 return Arrays.stream(data).sum(); } // 分解任务 int mid = data.length / 2; OuterTask left = new OuterTask(Arrays.copyOfRange(data, 0, mid)); OuterTask right = new OuterTask(Arrays.copyOfRange(data, mid, data.length)); left.fork(); return right.compute() + left.join(); } } public static void main(String[] args) { // 创建嵌套线程池 ForkJoinPool outerPool = new ForkJoinPool(4); ForkJoinPool innerPool = new ForkJoinPool(2); int[] data = IntStream.range(1, 100).toArray(); // 提交外层任务 outerPool.submit(() -> { // 执行外层分解 OuterTask outerTask = new OuterTask(data); outerTask.fork(); // 在内层池执行子任务 innerPool.submit(() -> { System.out.println("内层任务结果: " + new InnerTask(10).compute()); }); return outerTask.join(); }).join(); // 等待外层任务完成[^1] // 关闭线程池 outerPool.shutdown(); innerPool.shutdown(); } } ``` #### 关键注意事项 1. **避免死锁**: - 确保内层任务不会阻塞外层线程 - 使用 `join()` 时注意任务依赖关系 ```java // 正确写法:先fork再compute left.fork(); return right.compute() + left.join(); ``` 2. **资源隔离**: - 为不同层级使用独立线程池 - 限制各层线程池大小 ```java // 根据CPU核心数设置 int cores = Runtime.getRuntime().availableProcessors(); ForkJoinPool outer = new ForkJoinPool(cores / 2); ForkJoinPool inner = new ForkJoinPool(cores / 4); ``` 3. **任务粒度控制**: - 外层处理大块数据分解 - 内层处理精细计算 - 设置合理的阈值避免过度分解 4. **异常处理**: ```java try { innerPool.submit(...).get(); } catch (InterruptedException | ExecutionException e) { // 处理内层任务异常 } ``` #### 适用场景 1. **多层递归算法**:如复杂数学计算、图像处理 2. **混合并行任务**:CPU密集型 + I/O密集型组合 3. **分级数据处理**:先粗粒度分区再细粒度计算 > **重要提示**:嵌套线程池会增加系统复杂性,仅在单层线程池无法满足性能需求时使用。对于简单任务,优先使用单层 `ForkJoinPool` 或 `CompletableFuture`[^3]。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值