Mastering Asynchronous Batch Processing with JDK 21 Virtual Threads

Using JDK 21’s standardized Virtual Threads, this guide explains how to design and implement robust asynchronous batch processing, covering common pitfalls like CPU spikes and OOM, best‑practice concurrency controls, task queue architecture, and practical code illustrations.

Eric Tech Circle
Eric Tech Circle
Eric Tech Circle
Mastering Asynchronous Batch Processing with JDK 21 Virtual Threads

Introduction

During business system development, large‑scale data processing can cause CPU spikes, OOM, or overload external services.

CPU spikes leading to system lag or crash.

OOM causing endless restarts.

Excessive requests breaking databases or external systems.

This article shows how to use the newly standardized Virtual Threads in JDK 21 to implement asynchronous batch processing.

Best Practices for Using JDK 21 Virtual Threads

Create virtual threads synchronously; they are cheap and not scarce.

One virtual thread per concurrent task; no pooling needed.

Control concurrency with a Semaphore.

Avoid ThreadLocal as it may consume memory and is not thread‑safe.

Prevent carrier thread suspension; avoid synchronized or Object.wait() inside virtual thread code.

Asynchronous Batch Processing Design

Identify business operations that can be executed asynchronously (e.g., SMS, email, verification codes, internal messages, bulk invoice callbacks).

Define a task message containing task ID, type (enum), and JSON payload; bind each type to a handler and a semaphore.

Persist the task message to a queue (e.g., JDK BlockingQueue, Redisson, RocketMQ). Optionally use a delay queue to schedule future execution.

Run a carrier thread with an infinite while loop that polls the queue; block when the queue is empty.

Before launching a virtual thread, acquire a semaphore permit for the task type; block the carrier thread if permits are unavailable.

Each task type maps to a separate queue topic and its own carrier thread, preventing semaphore contention across types.

Start a virtual thread to execute the business logic defined by the task‑type enum; release the semaphore in a finally block.

Classify task types (CPU‑bound, I/O‑bound, resource‑limited) and tune semaphore limits accordingly to avoid CPU 100 %, OOM, or external system crashes.

If a task throws an exception, keep the carrier loop running, log the error, and design an automatic retry strategy.

Appendix

Code Structure

Code structure diagram
Code structure diagram

Task Type Design

Task type code design
Task type code design

Semaphore Configuration

Semaphore settings
Semaphore settings

Testing

The test uses JDK's LinkedBlockingQueue as the in‑memory queue. Because a multi‑topic mode is not implemented, semaphore permits for different task types may compete.

Test result
Test result

Concurrent Result Retrieval

Concurrent result collection
Concurrent result collection
Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

BackendJavaconcurrencyJDK21VirtualThreadsBatchProcessingAsynchronousProcessing
Eric Tech Circle
Written by

Eric Tech Circle

Backend team lead & architect with 10+ years experience, full‑stack engineer, sharing insights and solo development practice.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.