skip to content

Multithreaded Step

Handing a step a task executor runs chunks concurrently in one JVM, but most readers are stateful, so you need a thread-safe or synchronized reader. Interviewers ask what breaks first, and the shared reader is the answer.

part ofSpring Frameworkoverview, primer and where to startread it →
on this pageshow

explore

questions

5

What is a multithreaded step in Spring Batch, and how do you enable it?

level: juniorimportance: must knowfreq 55%

answer

  1. taskExecutor(...) on the step builder
  2. one shared reader across threads
  3. SynchronizedItemStreamReader for stateful readers
  4. throttleLimit = chunks in flight (deprecated in 5)
  5. no ordering / weak restart

basics

~20 s

A multithreaded step processes chunks on several threads inside one JVM instead of one. You enable it by giving the step a TaskExecutor (e.g. step.taskExecutor(...)), so each chunk runs on a separate thread from the pool.

solid answer

~40 s

A multithreaded step is Spring Batch's simplest scaling technique: within a single step and single JVM, chunks are dispatched to a thread pool instead of being processed sequentially on one thread. You configure it in the StepBuilder with .taskExecutor(taskExecutor), passing a Spring TaskExecutor such as a ThreadPoolTaskExecutor (never SyncTaskExecutor, which stays single-threaded). Each worker thread reads a chunk, processes and writes it concurrently with other threads. Because the same ItemReader instance is shared across threads, the big caveat is that the reader (and any stateful component) must be thread-safe — most built-in readers are not, so you wrap them in SynchronizedItemStreamReader. throttleLimit historically capped the number of concurrent chunks. It boosts throughput on I/O-bound steps without the setup cost of partitioning or remote workers.

code

java · 22 lines
java
@Bean
public Step multithreadedStep(JobRepository jobRepository,
                               PlatformTransactionManager tx,
                               ItemReader<String> delegate,
                               ItemWriter<String> writer) {
    // Wrap the stateful reader so read() is synchronized
    SynchronizedItemStreamReader<String> safeReader = new SynchronizedItemStreamReader<>();
    // NOTE: delegate must be an ItemStreamReader for this to compile
    // safeReader.setDelegate((ItemStreamReader<String>) delegate);

    ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
    executor.setCorePoolSize(8);
    executor.setMaxPoolSize(8);
    executor.afterPropertiesSet();

    return new StepBuilder("multithreadedStep", jobRepository)
            .<String, String>chunk(100, tx)
            .reader(safeReader)
            .writer(writer)
            .taskExecutor(executor)   // dispatch chunks across the pool
            .build();
}

go deeper

for a junior

Know it's the simplest scaling option: add a TaskExecutor to the step so chunks run on multiple threads in one JVM.

for a middle

Explain the shared-reader hazard and SynchronizedItemStreamReader; know ThreadPoolTaskExecutor vs SyncTaskExecutor.

for a senior

Discuss throttleLimit (deprecated in 5), ordering loss, weakened restartability, and I/O-bound vs CPU-bound suitability.

for a principal

Weigh multithreaded step against partitioning/remote chunking; reason about restart semantics, idempotency, and when the shared-state risk outweighs the simplicity.

## The problem it solves By default a Spring Batch **chunk-oriented step** runs on a single thread: it reads N items (the chunk size), processes them, writes them in one transaction, commits, then repeats — strictly sequentially. If each write is I/O-bound (a DB insert, an HTTP call), the single thread spends most of its time waiting, leaving CPU idle. A **multithreaded step** lets multiple chunks be processed **concurrently on several threads inside the same JVM**. ## How to enable it You attach a Spring `org.springframework.core.task.TaskExecutor` to the step via the builder: ```java new StepBuilder("myStep", jobRepository) .<In, Out>chunk(100, transactionManager) .reader(reader).processor(processor).writer(writer) .taskExecutor(taskExecutor) // <-- makes it multithreaded .build(); ``` Internally the step's `RepeatTemplate` (a `TaskExecutorRepeatTemplate`) submits each chunk iteration to the executor. Use a real pooled executor like `ThreadPoolTaskExecutor`. If you pass `SyncTaskExecutor` (the default) it runs on the caller thread and stays single-threaded. ## The core gotcha: shared, stateful components There is exactly **one** `ItemReader` instance for the step, shared by every worker thread. Most built-in readers are **stateful** — e.g. `JdbcCursorItemReader` walks a JDBC cursor, `FlatFileItemReader` tracks the current line, `JpaPagingItemReader` tracks a page counter. Calling `read()` from multiple threads without synchronization corrupts that state: skipped rows, duplicates, `ArrayIndexOutOfBoundsException`, etc. The fix is `org.springframework.batch.item.support.SynchronizedItemStreamReader`, which wraps your reader and makes `read()` `synchronized`: ```java SynchronizedItemStreamReader<T> safe = new SynchronizedItemStreamReader<>(); safe.setDelegate(realReader); ``` (There is a matching `SynchronizedItemStreamWriter` if your writer is not thread-safe.) Note synchronizing `read()` serializes the reading — the parallelism win comes from processing and writing running concurrently, not reading. ## throttleLimit `throttleLimit` bounds how many chunks may be **in flight** (submitted but not yet completed) at once, independent of the executor's own pool size. Classically it defaulted to 4. In Spring Batch 5 the `throttleLimit(int)` builder method is **deprecated**; the recommended control is to size the `TaskExecutor`'s pool (core/max pool size, queue capacity) directly. Conceptually it prevents the step from flooding the executor's queue faster than workers can drain it. ## Ordering and restart caveats - **No ordering guarantee.** Chunks are processed concurrently, so items are read, written and committed out of order. Don't use it if downstream logic depends on input order. - **Restartability is weakened.** Spring Batch tracks progress by a single read-count in `BATCH_STEP_EXECUTION`. With concurrency, if the job fails mid-flight there can be gaps (chunk 5 committed but chunk 4 didn't), so re-running may reprocess or skip. For this reason multithreaded steps are best for **restart-from-scratch** / idempotent workloads, and `saveState(false)` is sometimes set on readers. - **Not fault-tolerant-friendly.** Combining with skip/retry and stateful readers multiplies the concurrency hazards. ## When to use vs alternatives Use a multithreaded step for a quick throughput win on **one JVM**, **I/O-bound** steps, where strict ordering and exact restart aren't required. When you need bounded data partitions, per-partition restartability, or to scale across JVMs, prefer **local/remote partitioning** (`Partitioner` + `PartitionHandler`) or **remote chunking**, which give each worker its own reader instance and avoid the shared-state problem entirely.

  • Why can't you just pass any TaskExecutor and expect a speedup?
    SyncTaskExecutor (the default) runs the chunk on the caller's thread, so it stays single-threaded. You need a truly pooled executor like ThreadPoolTaskExecutor, and the step must actually be I/O-bound to benefit.
  • What breaks if the reader isn't made thread-safe?
    Multiple threads call read() on one stateful reader concurrently, corrupting its internal cursor/line/page counter — causing duplicate items, skipped items, or exceptions. Wrap it in SynchronizedItemStreamReader.

saying these in an interview costs you the question

  • Thinking a multithreaded step spins up multiple JVMs (it's one JVM, one process)
  • Assuming each thread gets its own reader instance (there's one shared reader)
  • Believing SyncTaskExecutor gives parallelism
  • Expecting items to stay in input order

context

open as a page

Why are most Spring Batch ItemReaders unsafe in a multithreaded step, and how do you fix it?

level: seniorimportance: must knowfreq 45%

basics

~20 s

There is one reader instance shared by all threads, and most readers hold mutable state (a cursor, current line, or page index). Concurrent read() calls corrupt that state. Wrap the reader in SynchronizedItemStreamReader so read() is synchronized.

open as a page

What does throttleLimit control in a multithreaded step, and how has its role changed in Spring Batch 5?

level: middleimportance: should knowfreq 35%

basics

~20 s

throttleLimit caps how many chunks can be processed concurrently (in flight at once), regardless of pool size. In Spring Batch 5 the throttleLimit(...) builder method is deprecated; you instead size the TaskExecutor's thread pool directly.

open as a page

What are the restartability, ordering, and fault-tolerance limitations of a multithreaded step, and when should you avoid it?

level: seniorimportance: should knowfreq 30%

basics

~20 s

Chunks run concurrently, so items are processed out of order and commit out of order. On failure the single read-count can't cleanly mark a resume point, so restart may skip or reprocess items. Avoid it when order matters, restart must resume exactly, or you rely heavily on skip/retry.

open as a page

Compare a multithreaded step with partitioning and remote chunking. When would you choose each?

level: principalimportance: should knowfreq 30%

basics

~20 s

Multithreaded step: many threads share one reader in one JVM — simplest, but shared state and weak restart. Partitioning: each worker step gets its own reader over a data slice — clean restart, scales locally or across JVMs. Remote chunking: one master reads, workers process/write remotely — for CPU/write-heavy work.

open as a page