百科.dev
全部条目AI 编程趋势榜开源项目技术资讯提交条目
登录
< 返回工具列表
P

parallel-collectors

> 编程语言
开源

Parallel Collectors 是一个工具包,可通过 Stream API 简化 Java 中的并行集合处理。

679 stars0 点赞0 次浏览
访问官网GitHub

工具介绍

Parallel Collectors 是一个工具包,可通过 Stream API 简化 Java 中的并行集合处理。

Java Stream API Virtual-Threads-enabled Parallel Collectors

Overcoming limitations of standard Parallel Streams Parallel Collectors is a toolkit that eases parallel collection processing in Java using the Stream API without the limitations imposed by standard Parallel Streams.

list.stream()
  .collect(parallel(i -> blockingOp(i), toList()))
    .orTimeout(1000, MILLISECONDS)
    .thenAcceptAsync(System.out::println, executor)
    .thenRun(() -> System.out.println("Finished!"));

They are:

  • lightweight, defaulting to Virtual Threads (an alternative to Project Reactor for scenarios where a lighter solution is preferred)
  • powerful (the combined power of Stream API and CompletableFutures, allowing for timeout specification, composition with other CompletableFutures, and asynchronous processing)
  • configurable (flexibility with customizable Executors and parallelism levels)
  • non-blocking (eliminates the need to block the calling thread while awaiting results)
  • short-circuiting (if one of the operations raises an exception, the remaining tasks will get interrupted)
  • non-invasive (they are just custom implementations of Collector interface, no magic inside, zero-dependencies, no Stream API internals hacking)
  • versatile (enables easy integration with existing Stream API Collectors)

Used by

  • Jenkins JUnit Plugin — the official Jenkins plugin for publishing JUnit test results
  • LinkedIn Avro Util — LinkedIn's utilities for working across multiple Apache Avro versions

Maven Dependencies

JDK 21+:

    com.pivovarit
    parallel-collectors
    4.0.0

JDK 8+:

    com.pivovarit
    parallel-collectors
    2.6.1
Gradle

JDK 21+:

implementation 'com.pivovarit:parallel-collectors:4.0.0'

JDK 8+:

implementation 'com.pivovarit:parallel-collectors:2.6.1'

Philosophy

Parallel Collectors are intentionally unopinionated, leaving responsibility to users for:

  • Proper configuration of provided Executors and their lifecycle management
  • Choosing appropriate parallelism levels
  • Ensuring the tool is applied in the right context

Review the API documentation before deploying in production.

Why This Exists?

The goal is to use the Stream API without inheriting the limitations of parallel streams, especially for I/O-heavy or structured workloads.

Java's built-in parallelization story is geared toward CPU-bound workloads - parallelStream() runs everything on the shared ForkJoinPool, which makes it a poor fit for blocking I/O, remote calls, database access, or anything that can stall a worker thread. Once that pool is saturated, everything else using it slows down as well.

This library fills that gap. It keeps the Stream API model but replaces the execution strategy:

  • user-provided executors instead of the common pool
  • virtual-thread defaults for low-overhead concurrency
  • classification and batching for further scheduling fine-tuning
  • CompletableFuture integration so you can work asynchronously and apply timeouts, callbacks, or composition naturally

Basic API

The main entry point is the com.pivovarit.collectors.ParallelCollectors class - which follows the convention established by java.util.stream.Collectors and features static factory methods returning custom java.util.stream.Collector implementations spiced up with parallel processing capabilities.

By default, collectors use Virtual Threads, but you can optionally provide a custom Executor instance for more control. When using a custom Executor, you are responsible for its lifecycle management.

All parallel collectors are one-off and must not be reused.

Important: parallel(mapper) returns CompletableFuture>, not CompletableFuture>. If you want a List, pass a downstream collector explicitly: parallel(mapper, toList()). The same applies to parallelBy.

Choosing the Right Collector

flowchart TD

A[Are you ok blocking the caller thread while waiting for processing to finish?] -->|No| B[Use ParallelCollectors.parallel]
A -->|Yes| C{Does the order of elements matter?}

C -->|Yes| D["Use ParallelCollectors.parallelToStream with c -> c.ordered()"]
C -->|No| E[Use ParallelCollectors.parallelToStream]

ParallelCollectors.parallel family returns CompletableFuture while ParallelCollectors.parallelToStream family returns Stream.

Additionally, you can customize:

  • a custom Executor (defaults to Virtual Threads)
  • a custom parallelism level
  • batching via the batching() configurer option
  • grouping by key via parallelBy(...) / parallelToStreamBy(...) methods
  • ordered streaming via the ordered() configurer option (streaming collectors only)
  • a custom downstream Collector (ParallelCollectors.parallel only)
  • executor decoration via executorDecorator() to wrap the resolved executor
  • task decoration via taskDecorator() to wrap each individual task

All configuration is done via the CollectingConfigurer (for parallel/parallelBy) or StreamingConfigurer (for parallelToStream/parallelToStreamBy) passed as a Consumer:

list.stream()
  .collect(parallel(i -> foo(i), c -> c
    .executor(executor)
    .parallelism(4)
    .batching(),
  toList()));

Batching Collectors

When you use non-batching parallel collectors, every input element is turned into an individual task submitted to an ExecutorService. If you have 1000 elements, you end up submitting 1000 tasks. Even if you only have two threads processing them, both threads hammer the same task queue, repeatedly competing for the next piece of work. That competition creates contention, and overall overhead.

This behaviour resembles a primitive form of work-stealing, where each worker repeatedly tries to grab the next available task. Work-stealing is great in scenarios where task durations vary significantly, since it keeps faster workers busy, but it's not free.

However, if the processing time for all subtasks is similar, it might be better to distribute tasks in batches to avoid excessive contention.

Without batching:

Thread 1: [] [] [] [] [] [] [] [] [] [] [] ... (500 tiny tasks)
Thread 2: [] [] [] [] [] [] [] [] [] [] [] ... (500 tiny tasks)

With batching:

Thread 1: [--------------------------------------------------] (1 large task)
Thread 2: [--------------------------------------------------] (1 large task)

The difference in performance for lightweight tasks can be enormous:

Benchmark                              Mode  Cnt      Score     Error  Units
BatchedVsNonBatchedBenchmark.batch    thrpt    5  41558.548 ± 959.057  ops/s
BatchedVsNonBatchedBenchmark.normal   thrpt    5    254.869 ±   5.667  ops/s

Batching can be enabled via the batching() configurer option:

list.stream()
  .collect(parallel(i -> foo(i), c -> c.parallelism(4).batching(), toList()));

Normal

Batched

Grouping Collectors

The parallelBy(...) and parallelToStreamBy(...) methods allow you to classify input elements by a key and process each group in parallel. Each group is guaranteed to be processed on a single thread, and results are returned as Group entries:

CompletableFuture>> result = tasks.stream()
  .collect(parallelBy(Task::groupId, t -> compute(t)));

CompletableFuture>> result = tasks.stream()
  .collect(parallelBy(Task::groupId, t -> compute(t), toList()));

The Group record provides key() and values() accessors, plus a map() method for transforming values while preserving the grouping key.

Decorators

Two decorator options let you add cross-cutting behavior without replacing the executor:

executorDecorator(UnaryOperator) wraps the resolved executor (the virtual-thread default or a custom one) and returns a replacement. It is invoked once per collector, before any tasks are submitted. This is a natural fit for intercepting every execute() call, for example to plug in a monitoring layer.

The returned executor must not drop or discard tasks — doing so will cause the collector to wait indefinitely for results that will never arrive.

list.stream()
  .collect(parallel(i -> foo(i), c -> c
    .executorDecorator(exec -> task -> {
        metrics.incrementAndGet();
        exec.execute(task);
    }),
  toList()));

taskDecorator(UnaryOperator) wraps each individual task before it is handed to the executor. Unlike the executor decorator, it runs on the worker thread and is re-applied for every element. This makes it the right tool for propagating thread-local context (MDC, OpenTelemetry spans, SecurityContext) into worker threads:

var snapshot = MDC.getCopyOfContextMap();

list.stream()
  .collect(parallel(i -> foo(i), c -> c
    .taskDecorator(task -> () -> {
        MDC.setContextMap(snapshot);
        try {
            task.run();
        } finally {
            MDC.clear();
        }
    }),
  toList()));

Both decorators can be combined and each may be specified at most once per configurer.

Leveraging CompletableFuture

Parallel Collectors expose results wrapped in CompletableFuture instances, which provides great flexibility and the possibility of working with them in a non-blocking fashion:

CompletableFuture> result = list.stream()
  .collect(parallel(i -> foo(i), toList()));

This makes it possible to conveniently apply callbacks and compose with other CompletableFutures:

list.stream()
  .collect(parallel(i -> foo(i), toSet()))
  .thenAcceptAsync(System.out::println, otherExecutor)
  .thenRun(() -> System.out.println("Finished!"));

Or just join() if you just want to block the calling thread and wait for the result:

List result = list.stream()
  .collect(parallel(i -> foo(i), toList()))
  .join();

What's more, since JDK9, you can even provide your own timeout easily.

Examples

1. Apply i -> foo(i) in parallel using Virtual Threads and collect to List
CompletableFuture> result = list.stream()
  .collect(parallel(i -> foo(i), toList()));
2. Apply i -> foo(i) in parallel on a custom Executor with max parallelism of 4 and collect to Set
Executor executor = ...

CompletableFuture> result = list.stream()
  .collect(parallel(i -> foo(i), c -> c
    .executor(executor)
    .parallelism(4),
  toSet()));
3. Apply i -> foo(i) in parallel with batching and collect to LinkedList
CompletableFuture> result = list.stream()
  .collect(parallel(i -> foo(i), c -> c.parallelism(4).batching(),
    toCollection(LinkedList::new)));
4. Apply i -> foo(i) in parallel and stream results in completion order
list.stream()
  .collect(parallelToStream(i -> foo(i)))
  .forEach(i -> ...);
5. Apply i -> foo(i) in parallel and stream results in the original order
list.stream()
  .collect(parallelToStream(i -> foo(i), c -> c.ordered()))
  .forEach(i -> ...);
6. Classify and process elements in parallel by group
CompletableFuture>> result = tasks.stream()
  .collect(parallelBy(Task::groupId, t -> compute(t)));
7. Apply i -> foo(i) in parallel with full configuration
Executor executor = ...

CompletableFuture> result = list.stream()
  .collect(parallel(i -> foo(i), c -> c
    .executor(executor)
    .parallelism(64)
    .batching(),
  toList()));
8. Propagate MDC context into worker threads via taskDecorator
var snapshot = MDC.getCopyOfContextMap();

CompletableFuture> result = list.stream()
  .

Issues· 0 开放

查看全部 Issues在 GitHub 打开

暂无开放 Issues,或尚未同步最近议题。

> 标签

Javahacktoberfestparallel-streamsparallelismstream-api

暂无评论,来聊聊你的看法吧

> 工具信息

发布日期2026年8月1日
最后更新2026年9月17日
分类编程语言
定价开源

> 相关工具

T
TypeScript
JavaScript 的超集,为前端与全栈提供静态类型
P
Python
通用编程语言,广泛用于 Web、数据与 AI
G
Go
Google 推出的简洁高效系统语言