Java concurrency is the topic that trips up senior engineers more than almost anything else in the ecosystem. It is not that the APIs are hard to read — CompletableFuture.thenCompose() is not a complicated method signature. The problem is that the consequences of getting it wrong are invisible until production load hits: a thread pool sized for 50 concurrent requests that quietly saturates at 200, a CompletableFuture chain that swallows exceptions because no one added exceptionally(), a fan-out that completes 99 of 100 calls in 50ms and then waits 30 seconds for the one that timed out.
I have spent more time debugging Java concurrency bugs than I care to admit. The ones that hurt most were not deadlocks — those are at least loud. They were the silent ones: a shared SimpleDateFormat causing random NumberFormatException in production, a ForkJoinPool getting saturated by blocking I/O calls that belonged in a separate executor, a CompletableFuture chain that worked perfectly in tests because the test executor ran everything synchronously. These 20 prompts encode the diagnostic questions and implementation patterns I reach for in those situations.
AI assistants are unusually well-suited to Java concurrency work. They know the java.util.concurrent API surface in full, understand the difference between thenApply and thenApplyAsync, can read a thread dump and identify the contention point, and will not lose track of which executor each stage of a pipeline is running on. The prompts below are structured to give the AI exactly what it needs to help: the right diagnostic data for debugging prompts, and the right context (pool sizes, workload type, latency targets) for implementation prompts.
This post gives you 20 copy-paste AI prompts for Java concurrency and CompletableFuture, progressing from fundamentals through advanced production patterns and into debugging. Each prompt is explained so you know when to reach for it and what context to provide for the best output.
For background on the underlying mechanics, see the Java Concurrency Deep Dive and Virtual Threads vs Platform Threads benchmarks. For the performance analysis layer, see 10 AI Prompts for Java Performance Optimization.
Quick Reference: All 20 Prompts at a Glance
| # | Prompt Title | Level | Use When |
|---|---|---|---|
| 1 | Size an ExecutorService for your workload | Basic | Creating a new thread pool |
| 2 | Convert a sequential loop to CompletableFuture fan-out | Basic | Parallelising independent tasks |
| 3 | Chain dependent async operations correctly | Basic | Multi-step async pipelines |
| 4 | Add timeout and fallback to a CompletableFuture | Intermediate | Preventing unbounded waits |
| 5 | Handle exceptions in a CompletableFuture chain | Intermediate | Silently-swallowed exceptions |
| 6 | Implement a rate-limited async task runner | Intermediate | Throttling outbound API calls |
| 7 | Build a fan-out/fan-in aggregator | Intermediate | Combining results from multiple sources |
| 8 | Migrate from Future to CompletableFuture | Intermediate | Modernising legacy async code |
| 9 | Implement a bulkhead pattern with separate pools | Advanced | Isolating failure domains |
| 10 | Design a work-stealing pipeline | Advanced | CPU-bound parallel processing |
| 11 | Migrate blocking I/O to virtual threads | Advanced | Scaling I/O-bound services |
| 12 | Implement structured concurrency with StructuredTaskScope | Advanced | Lifecycle-safe fan-out (Java 25) |
| 13 | Build a reactive backpressure pipeline | Advanced | Flow-controlled async processing |
| 14 | Implement a concurrent cache with read/write lock | Advanced | High-read, low-write shared state |
| 15 | Debug: diagnose thread dump contention | Debugging | Unresponsive / high-latency service |
| 16 | Debug: find a CompletableFuture that never completes | Debugging | Hung async operations |
| 17 | Debug: identify thread pool exhaustion | Debugging | All requests timing out under load |
| 18 | Debug: detect race conditions and visibility bugs | Debugging | Intermittent / flaky concurrency bugs |
| 19 | Debug: find deadlocks in a running JVM | Debugging | Complete application freeze |
| 20 | Full concurrency audit of a service class | Audit | Pre-release review / code handoff |
How to Get the Most From These Prompts
Concurrency prompts fail when context is missing. An AI asked to “size my thread pool” produces a textbook formula. An AI asked to size a pool for a service that makes 4 downstream HTTP calls per request, averages 120ms per call, runs in a Kubernetes pod with 2 CPU cores, and targets 500 req/s produces a specific number with a justification you can defend in a code review. The same principle applies to debugging prompts: paste the actual thread dump, not a description of it. The prompts below mark every placeholder that needs real values — fill them in before using.
Part 1: Fundamentals (Prompts 1–5)
Prompt 1: Size an ExecutorService for Your Workload
When to use: You are creating a new thread pool and want a calculated pool size rather than an arbitrary number like 10 or 50. Undersized pools cause request queuing; oversized pools waste memory and create context-switching overhead.
"Calculate the optimal ExecutorService thread pool size for the following workload.
Workload profile:
- Service type: [e.g. REST API handling order processing]
- Per-request work breakdown: [e.g. 5ms CPU computation + 120ms DB query + 80ms external HTTP call]
- CPU time per request: [e.g. ~5ms]
- I/O blocking time per request: [e.g. ~200ms total]
- Target throughput: [e.g. 500 requests/second]
- Pod CPU cores: [e.g. 2 vCPU]
- Deployment: [e.g. 4 Kubernetes pods]
Apply Little's Law: pool_size = target_concurrency × (CPU_time + IO_time) / CPU_time
Also apply the CPU formula: pool_size = core_count × (1 + IO_wait / CPU_time)
Show:
1. The calculation with the formula substituted
2. The recommended pool size per pod
3. The recommended queue capacity (LinkedBlockingQueue size) to absorb bursts
4. The recommended rejection policy (CallerRunsPolicy vs AbortPolicy) and why
5. The ThreadFactory configuration: name pattern, daemon vs non-daemon, UncaughtExceptionHandler
6. Whether to use a fixed pool, cached pool, or custom ThreadPoolExecutor — and why
7. The Micrometer metrics to add so the pool is observable in production:
- Active thread count
- Queue depth
- Completed task count
- Rejected task count
Also show the complete ThreadPoolExecutor constructor call with all parameters named and commented."
Prompt 2: Convert a Sequential Loop to CompletableFuture Fan-Out
When to use: You have a loop that calls a method independently for each item in a list and the calls are logically parallel — there are no dependencies between iterations. Converting to a fan-out can reduce wall-clock time from O(n × latency) to O(max latency).
"Convert the following sequential loop to a CompletableFuture fan-out pattern.
Sequential code to convert:
[PASTE YOUR FOR LOOP OR SEQUENTIAL STREAM HERE]
Context:
- What each iteration does: [e.g. calls an external inventory API to check stock for one SKU]
- Are the calls independent? [yes/no — if no, stop; fan-out does not apply]
- Expected latency per call: [e.g. 50–150ms]
- List size range: [e.g. 5–50 items per request]
- Thread pool to use: [e.g. dedicated HTTP client pool with 20 threads, or Executors.newVirtualThreadPerTaskExecutor()]
Generate:
1. The fan-out using CompletableFuture.supplyAsync(task, executor) for each item
2. CompletableFuture.allOf(futures.toArray(...)) to wait for all results
3. Result collection after allOf completes:
futures.stream().map(CompletableFuture::join).collect(Collectors.toList())
4. A per-call timeout using orTimeout(2, TimeUnit.SECONDS) before allOf — so a single slow call
does not block the entire fan-out
5. Exception handling: if ANY call fails, what should happen?
Option A: fail fast — propagate the first exception (default allOf behaviour)
Option B: partial results — collect successes, log failures, return what arrived
Show both implementations and explain the trade-off
6. A JUnit test that verifies the fan-out completes faster than the sum of individual latencies
(use a mock that introduces a 100ms delay per call and verify total < 200ms for 5 calls)"
Prompt 3: Chain Dependent Async Operations Correctly
When to use: You need to chain async operations where each step depends on the result of the previous step. The most common mistake here is confusing thenApply (synchronous transform) with thenCompose (flat-mapping a nested future) and accidentally running blocking code on the common pool.
"Design a CompletableFuture pipeline for the following sequence of dependent async operations.
Operations in order (each depends on the previous result):
[DESCRIBE EACH STEP, e.g.:
Step 1: Load user by userId from the database (async, returns User)
Step 2: Using the user's tier, fetch the correct pricing rules from a config service (async, returns PricingRules)
Step 3: Calculate the discounted price using User + PricingRules (synchronous CPU work)
Step 4: Persist the priced order to the database (async, returns OrderId)
Step 5: Send a confirmation email using the OrderId (async, fire-and-forget)]
Executor strategy:
- DB calls: use dbExecutor (dedicated pool, blocks on JDBC)
- HTTP calls: use httpExecutor (dedicated pool)
- CPU work: run on calling thread (no executor — use thenApply, not thenApplyAsync)
- Fire-and-forget: use emailExecutor, do not block the main chain
For each step, show:
1. The correct operator:
- thenApply: synchronous transform, same thread as upstream
- thenApplyAsync: synchronous transform, switches to a new executor thread
- thenCompose: async step that returns a CompletableFuture (use when the step IS async)
- thenComposeAsync: async step + explicit thread pool switch
- thenAccept / thenRun: terminal operations with no return value
2. WHY that operator is correct for this step
3. The exception the WRONG operator would cause (e.g., blocking the ForkJoinPool common pool)
Generate:
- The complete pipeline code
- A diagram (as ASCII) showing which executor each stage runs on
- The correct placement of exceptionally() to handle failures at each stage without losing the chain
- A unit test using CompletableFuture.completedFuture() stubs to test the pipeline logic
without needing real executors"
Prompt 4: Add Timeout and Fallback to a CompletableFuture
When to use: An async operation calls an external service and there is no upper bound on how long it can take. Without a timeout, a single slow downstream can hold threads indefinitely and eventually exhaust the thread pool. This is one of the most common production concurrency bugs.
"Add timeout handling and a fallback strategy to the following CompletableFuture operation.
Current code (the async operation that needs a timeout):
[PASTE YOUR CompletableFuture CODE HERE]
Requirements:
- Maximum wait time: [e.g. 3 seconds — if the operation takes longer, do not wait]
- Fallback behaviour on timeout: [choose one or describe your own:
A: Return a cached/default value (e.g. return empty list, use last known value)
B: Throw a specific exception (e.g. ServiceTimeoutException with the operation name)
C: Return a partial result if some sub-operations completed
D: Retry once with a shorter timeout before falling back]
- Should the timeout cancel the underlying operation? [yes/no — cancellation releases the thread
but does NOT interrupt native blocking I/O, only cooperative cancellation points]
Generate:
1. orTimeout(duration, unit) for hard timeout that throws CompletionException(TimeoutException)
2. completeOnTimeout(fallbackValue, duration, unit) for soft timeout that returns a default value
3. When to use orTimeout vs completeOnTimeout — concrete decision rule
4. The ScheduledExecutorService-based alternative for Java < 9 compatibility:
ScheduledFuture timeout = scheduler.schedule(() -> future.completeExceptionally(...), ...)
5. Logging: log a WARN with the operation name, timeout threshold, and actual elapsed time
when a timeout fires — without using System.currentTimeMillis() in the hot path
6. A unit test that:
- Simulates a slow operation using CompletableFuture.supplyAsync(() -> { sleep(5000); return x; })
- Verifies the timeout fires at the correct threshold
- Verifies the fallback value (or exception) is returned, not the slow result
- Verifies the timeout does not cause a thread leak"
Prompt 5: Handle Exceptions in a CompletableFuture Chain
When to use: Exceptions in a CompletableFuture chain are silently swallowed if no handler is registered. The future enters an “exceptionally completed” state, every downstream thenApply/thenCompose is skipped, and the exception only surfaces when someone calls join() or get() — often much later, in a completely different part of the code. This prompt generates correct exception handling for every scenario.
"Audit and fix the exception handling in the following CompletableFuture chain.
Chain to audit:
[PASTE YOUR CompletableFuture CHAIN HERE]
Explain each exception handling operator and when to use it:
1. exceptionally(fn): recovers from ANY exception, returns a fallback value of the same type
- Use when: you want to continue the chain with a default value on any failure
- Pitfall: exceptionally() itself can throw, which replaces the original exception silently
2. handle(fn): called for BOTH success and failure, receives (result, exception) bifunction
- Use when: you need different logic for success vs failure in the same stage
- Unlike exceptionally(), handle() can transform the result type
3. whenComplete(action): side-effect only (logging, metrics), does NOT recover the exception
- Use when: you need to log failures without changing the chain's outcome
- The exception still propagates after whenComplete
4. exceptionallyCompose(fn): like exceptionally() but the fallback itself is async
- Use when: the fallback involves an async call (e.g. fetch from a backup service)
For the pasted chain, generate:
- A corrected version with exception handling at each stage that can fail independently
- Logging in whenComplete for every uncaught exception (with operation name, duration, error)
- The correct place to call join() or get() — and why calling it inside a thenApply is wrong
- A CompletableFuture.allOf() pattern that collects ALL exceptions from a fan-out, not just the first
- A JUnit test that verifies: (a) the fallback is returned on downstream service failure,
(b) the exception is logged, (c) a successful path is not affected by the exception handling code"
Part 2: Intermediate Patterns (Prompts 6–10)
Prompt 6: Implement a Rate-Limited Async Task Runner
When to use: You need to make a large number of async calls to an external API that enforces a rate limit (e.g. 100 calls/second). Submitting all calls at once will overwhelm the API and cause 429 errors. This prompt generates a throttled runner that respects the limit.
"Implement a rate-limited async task runner in Java using CompletableFuture.
Rate limiting requirements:
- Maximum calls per second: [e.g. 100 calls/second to an external enrichment API]
- Maximum concurrent in-flight calls: [e.g. 20 simultaneous HTTP connections]
- Total items to process: [e.g. 10,000 items from a database batch job]
- Per-item operation: [PASTE OR DESCRIBE THE ASYNC CALL, e.g. enrichmentClient.enrich(item)]
- On rate limit hit (HTTP 429): [retry after X seconds / exponential backoff / skip with log]
Generate:
1. A Semaphore-based concurrency limiter:
Semaphore semaphore = new Semaphore(maxConcurrent);
- Acquire before submit, release in whenComplete
- Show how to avoid permit leak if the task itself throws before submitting
2. A token bucket rate limiter using Guava RateLimiter or a custom implementation:
RateLimiter rateLimiter = RateLimiter.create(callsPerSecond);
- rateLimiter.acquire() before each submission — blocks the submission thread, not the executor
- Why this is better than Thread.sleep() for rate limiting
3. The full batch processing loop:
- Partition the 10,000 items into batches
- Submit each item as a CompletableFuture with rate limiter + semaphore
- Collect results with allOf() per batch, flush to database between batches
- Progress logging every 1,000 items
4. Retry logic for 429 responses:
- Detect HTTP 429 in exceptionally()
- Extract Retry-After header value
- Re-submit after the specified delay using CompletableFuture.delayedExecutor()
5. A JUnit test with a mock API that counts calls per second and asserts the rate is not exceeded"
Prompt 7: Build a Fan-Out / Fan-In Aggregator
When to use: A BFF (backend-for-frontend) or aggregator service must call multiple upstream services in parallel and combine their results into a single response. The challenge is handling partial failures — if one upstream is slow or down, should the whole response fail or return partial data?
"Build a CompletableFuture fan-out/fan-in aggregator for the following BFF endpoint.
Endpoint: [e.g. GET /api/v1/product-detail/{productId}]
Upstream calls to fan out (all independent, all in parallel):
[LIST EACH CALL, e.g.:
- Call 1: ProductService.getProduct(productId) — required, if fails return 404
- Call 2: InventoryService.getStock(productId) — optional, if fails return default {inStock: true}
- Call 3: PricingService.getPrice(productId, userId) — required, if fails return 503
- Call 4: ReviewService.getTopReviews(productId, limit=3) — optional, if fails return empty list
- Call 5: RecommendationService.getSimilar(productId) — optional, if fails return empty list]
Timeout per call: [e.g. 500ms for required, 300ms for optional]
Generate:
1. All five CompletableFuture.supplyAsync() submissions on their respective dedicated executors
2. A CompletableFuture for each call that wraps orTimeout() and a per-call fallback:
- Required calls: exceptionally(() -> { throw new ServiceUnavailableException(...); })
- Optional calls: exceptionally(ex -> defaultValue) with a WARN log
3. CompletableFuture.allOf(required1, required2, optional1, optional2, optional3)
- Wait for ALL to complete (success or fallback), not just the required ones
4. Result assembly in a thenApply after allOf:
- Extract each result with .join() (safe here because allOf guarantees completion)
- Build the aggregated ProductDetailResponse record
5. Response headers:
- X-Upstream-Latency: the max of all upstream call durations
- X-Partial-Response: true if any optional call used its fallback
6. A @WebMvcTest that verifies:
- Happy path returns all fields populated
- InventoryService timeout still returns a response with inStock=true default
- ProductService failure returns 503, not a partial response"
Prompt 8: Migrate from Future to CompletableFuture
When to use: Legacy code uses ExecutorService.submit() returning Future<T>. Future has no chaining, no callbacks, no exception handling, and Future.get() blocks the calling thread. This prompt automates the migration to CompletableFuture.
"Migrate the following legacy Future-based code to CompletableFuture.
Legacy code to migrate:
[PASTE YOUR Future-BASED CODE HERE]
Migration rules to apply:
1. ExecutorService.submit(Callable) -> CompletableFuture.supplyAsync(Supplier, executor)
- The Callable becomes a Supplier (no checked exception — wrap in try/catch or use a helper)
- The executor reference stays the same
2. future.get() blocking calls -> thenApply() / thenCompose() callbacks
- Find every future.get() call
- Replace it with a chained callback on the preceding CompletableFuture
- If the result is used in the next line, that becomes a thenApply()
- If the result triggers another async call, that becomes a thenCompose()
3. future.get(timeout, unit) -> orTimeout(timeout, unit)
4. Multiple Future.get() calls that block sequentially:
[future1.get(), future2.get(), future3.get()]
Replace with CompletableFuture.allOf(f1, f2, f3).thenApply(() -> collect results)
5. ExecutorCompletionService patterns -> CompletableFuture.anyOf() for first-completed semantics
6. Checked exception handling:
- Future.get() throws ExecutionException — unwrap its cause
- CompletableFuture exceptionally() receives the cause directly (already unwrapped)
For each replacement, show:
- The before code
- The after code
- The threading implication (which thread runs the callback vs the original blocking thread)
- Any behaviour change to be aware of (e.g. cancellation semantics differ)"
Prompt 9: Implement a Bulkhead Pattern with Separate Thread Pools
When to use: Multiple downstream dependencies share the same thread pool, so when one is slow (e.g. a reporting database query that runs long) it starves requests that need a different dependency (e.g. user auth). The bulkhead pattern isolates failure domains by giving each dependency its own pool with its own queue and rejection policy.
"Implement the Bulkhead pattern for the following service to isolate failure domains.
Service: [e.g. OrderService that depends on three downstream systems]
Current problem: [e.g. slow inventory queries block payment processing because both share the default pool]
Dependencies to bulkhead (one pool per dependency):
[LIST EACH DEPENDENCY, e.g.:
- InventoryService: average latency 50ms, peak 2s, called 3x per order, max concurrent: 30
- PaymentService: average latency 200ms, critical path, max concurrent: 10, never queue
- ShippingService: average latency 100ms, optional, max concurrent: 20, can queue up to 50]
For each dependency, generate:
1. A dedicated ThreadPoolExecutor with:
- corePoolSize, maximumPoolSize (calculated from latency and concurrency targets)
- keepAliveTime: 60s (let idle threads die)
- Queue: LinkedBlockingQueue(capacity) for queueable calls, SynchronousQueue for never-queue
- RejectionPolicy:
- Payment: AbortPolicy (reject immediately — never queue critical path)
- Shipping: CallerRunsPolicy (backpressure — slow the caller down)
- ThreadFactory with named threads: e.g. Thread.ofPlatform().name("inventory-pool-", 0)
2. A @Configuration class that declares each executor as a named @Bean:
@Bean("inventoryExecutor") public ExecutorService inventoryExecutor() { ... }
3. @Async integration: annotate service methods with @Async("inventoryExecutor") to route
each call to its dedicated pool automatically in Spring
4. Actuator metrics: expose queue depth and active thread count per pool via Micrometer
ThreadPoolTaskExecutor.getThreadPoolExecutor() → wrap in Gauge and Timer
5. A circuit breaker (Resilience4j) layered on top of each bulkhead:
- The bulkhead limits concurrency; the circuit breaker opens when error rate exceeds threshold
- Show the @Bulkhead + @CircuitBreaker stacked annotation order
6. A test that proves pool isolation:
- Fill the inventory pool to capacity (mock slow inventory calls)
- Verify payment calls still succeed within SLA (they have their own pool)"
Prompt 10: Design a Work-Stealing Pipeline for CPU-Bound Processing
When to use: You have a multi-stage processing pipeline where each stage has variable execution time (some items take much longer than others). A fixed thread pool leaves fast threads idle while slow threads hold up the queue. ForkJoinPool with work-stealing is designed for exactly this scenario.
"Design a work-stealing ForkJoinPool pipeline for the following CPU-bound processing job.
Job description: [e.g. Document ingestion pipeline: parse PDF → extract entities → embed vectors → write to vector DB]
Stages and their characteristics:
[LIST EACH STAGE, e.g.:
- Stage 1: PDF parsing — CPU-bound, 50–500ms per document, highly variable
- Stage 2: Entity extraction — CPU-bound, 10–200ms, uses an ML model
- Stage 3: Embedding — CPU-bound, 5–50ms, embarrassingly parallel
- Stage 4: DB write — I/O-bound, 10–30ms, must NOT run in ForkJoinPool]
Input: [e.g. List of 10,000 PDF files from S3]
Target throughput: [e.g. process all 10,000 documents in < 30 minutes]
Available cores: [e.g. 8 cores, 16 with hyperthreading]
Generate:
1. ForkJoinPool configuration:
- parallelism = available cores (NOT Runtime.getRuntime().availableProcessors() - 1 for pipelines)
- Why ForkJoinPool, not fixed ThreadPoolExecutor, for variable-duration tasks
- asyncMode=true for non-LIFO processing (FIFO is better for streaming pipelines)
2. RecursiveTask for the document processing chain:
- Override compute() to run the full pipeline for one document
- Use invokeAll() to submit batches of tasks at once
- Do NOT call blocking I/O inside compute() — extract Stage 4 to a separate executor
3. Stage 4 hand-off: after compute() finishes the CPU stages, submit the result to a
separate dbWriteExecutor (bounded ThreadPoolExecutor) — do not block in ForkJoinPool
4. Progress tracking: use AtomicLong counter + ScheduledExecutorService to log throughput
every 30 seconds without synchronization overhead
5. Checkpoint/restart: if the JVM dies mid-batch, how to resume from the last checkpoint
(show a simple file-based approach using a ConcurrentSkipListSet of processed IDs)
6. A benchmark comparing ForkJoinPool.commonPool() vs dedicated ForkJoinPool(8) vs
fixed ThreadPoolExecutor(8) for the variable-duration scenario"
Part 3: Advanced Patterns (Prompts 11–14)
Prompt 11: Migrate Blocking I/O to Virtual Threads
When to use: A service makes many blocking I/O calls (JDBC, HTTP, file) and is throttled by a platform thread pool. Virtual threads (Java 21+) can scale this kind of service to far higher concurrency without the overhead of reactive programming. This prompt audits existing code and produces the migration plan.
"Audit the following service for virtual thread migration suitability and produce the migration plan.
Service class(es) to audit:
[PASTE YOUR SERVICE CLASSES HERE]
Current concurrency configuration:
[PASTE application.yml or ThreadPoolExecutor config]
Workload profile:
- Request handling time: [e.g. 200ms total]
- Breakdown: [e.g. 10ms CPU, 150ms DB query, 40ms external HTTP call]
- Peak concurrent requests: [e.g. 600]
- Current thread pool size: [e.g. 200 Tomcat threads]
Step 1 — Suitability assessment:
1. I/O-bound ratio: [AI calculates] — virtual threads help when > 70% is blocking I/O
2. CPU-bound operations: identify any loops or computations that run > 1ms per request
— these will NOT benefit from virtual threads and should stay on platform threads
3. synchronized blocks wrapping blocking calls: CRITICAL — list every one
- Java 21-24: these pin the carrier thread (defeats virtual thread benefit)
- Java 25: JEP 491 fixes this — confirm target Java version
4. ThreadLocal usage: large ThreadLocal values create memory pressure at virtual thread scale
— identify candidates for ScopedValue migration
Step 2 — Migration:
5. For Spring Boot 3.2+: spring.threads.virtual.enabled=true (one property — show where)
6. For plain Java: replace Executors.newFixedThreadPool(N) with
Executors.newVirtualThreadPerTaskExecutor()
7. Replace synchronized blocks around blocking I/O with ReentrantLock
8. Add -Djdk.tracePinnedThreads=full to JVM args during testing to detect remaining pinning
Step 3 — Verification:
9. JMH benchmark comparing platform vs virtual threads for this specific workload
10. The counterexample: a CPU-bound test proving virtual threads give NO benefit for computation
— important to include so the team understands the limits"
Prompt 12: Implement Structured Concurrency with StructuredTaskScope
When to use: You need a fan-out where all forked tasks are automatically cancelled when the scope exits — a guarantee that CompletableFuture.allOf() cannot provide. StructuredTaskScope finalised in Java 25 is the right tool. This prompt covers both ShutdownOnFailure and ShutdownOnSuccess variants.
"Implement the following fan-out using Java 25 StructuredTaskScope (final API).
Fan-out operation: [DESCRIBE THE OPERATION, e.g.
'Fetch user profile, account balance, and recent transactions in parallel.
All three are required. If any one fails, cancel the others and propagate the exception.']
Variant to implement: [choose: ShutdownOnFailure / ShutdownOnSuccess / both]
For ShutdownOnFailure (all must succeed):
1. try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
Subtask userTask = scope.fork(() -> userService.getUser(userId));
Subtask balanceTask = scope.fork(() -> accountService.getBalance(userId));
Subtask<List> txnTask = scope.fork(() -> txnService.getRecent(userId, 10));
scope.join(); // waits for all; cancels others if any fails
scope.throwIfFailed(); // propagates first exception if any failed
return new UserSummary(userTask.get(), balanceTask.get(), txnTask.get());
}
2. Explain: when scope.join() returns, ALL subtasks are either done or cancelled
— no thread leaks, no orphaned tasks running after the scope exits
3. Compare with CompletableFuture.allOf(): show the allOf version and explain why it cannot
guarantee cancellation of running tasks
For ShutdownOnSuccess (first one wins):
4. try (var scope = new StructuredTaskScope.ShutdownOnSuccess()) {
scope.fork(() -> primaryCache.get(userId)); // try fast cache first
scope.fork(() -> databaseFallback.get(userId)); // backup
scope.join();
return scope.result(); // returns the first successful result
}
5. When to use ShutdownOnSuccess: hedged requests, cache-with-fallback, multi-region reads
Timeout integration:
6. scope.joinUntil(Instant.now().plusSeconds(2)) — cancels ALL tasks if the deadline passes
7. Handle TimeoutException from join(): log timeout + which subtasks were in-flight
Migration from CompletableFuture.allOf():
8. Show the before (allOf) and after (StructuredTaskScope) for the same operation
9. The key difference: StructuredTaskScope.fork() returns Subtask, not CompletableFuture
— Subtask.get() can only be called after scope.join() returns"
Prompt 13: Build a Reactive Backpressure Pipeline
When to use: A producer generates data faster than a consumer can process it. Without backpressure, the queue grows unboundedly and eventually causes OOM. This prompt implements a java.util.concurrent.Flow-based pipeline or a blocking-queue handoff that applies backpressure from consumer to producer.
"Implement a backpressure-aware producer-consumer pipeline using Java's built-in concurrency primitives.
Pipeline description:
- Producer: [e.g. reads messages from a Kafka topic, 10,000 messages/second peak]
- Consumer: [e.g. calls an enrichment API, max 200 calls/second due to API rate limit]
- The producer is faster than the consumer — backpressure is required
Approach A — BlockingQueue backpressure (simpler, recommended for most cases):
1. ArrayBlockingQueue queue = new ArrayBlockingQueue(capacity)
- capacity = consumer throughput × acceptable latency buffer
- e.g. 200 calls/s × 5 seconds buffer = 1,000 capacity
2. Producer uses queue.put(item) — BLOCKS when queue is full, which backpressures the producer
- This is correct: the producer slows down to match the consumer
3. Consumer uses queue.take() — BLOCKS when queue is empty (idle wait with no busy-spin)
4. How to choose capacity: too small = producer blocks too often; too large = latency grows
5. Multiple consumers: N consumer threads each calling queue.take() — show the pool setup
Approach B — java.util.concurrent.Flow (reactive, for composable pipelines):
6. Implement a Publisher that respects subscription.request(n) signals
7. Implement a Subscriber that calls subscription.request(batchSize) after processing
8. The demand-driven pull model: subscriber pulls only as many items as it can handle
9. Show SubmissionPublisher (JDK built-in) as a simple publisher implementation
Monitoring:
10. Log queue depth every 10 seconds using a ScheduledExecutorService
11. Alert when queue is > 80% full (producer is overwhelming consumer)
12. Metrics: producer throughput, consumer throughput, queue depth, rejection count"
Prompt 14: Implement a Concurrent Cache with Read/Write Lock
When to use: A shared data structure is read very frequently and written rarely. Using synchronized on reads blocks all concurrent readers even when no write is in progress. ReadWriteLock allows unlimited concurrent readers and exclusive writers, dramatically improving throughput for read-heavy workloads.
"Implement a thread-safe in-memory cache using ReadWriteLock with the following requirements.
Cache description: [e.g. stores product pricing rules that are read on every request and refreshed from the database every 5 minutes]
Access pattern:
- Read frequency: [e.g. ~500 reads/second]
- Write frequency: [e.g. 1 write every 5 minutes (background refresh)]
- Cache size: [e.g. ~10,000 entries]
- Eviction policy: [e.g. TTL-based, LRU, or none]
Generate:
1. ReadWriteLock implementation:
private final ReadWriteLock lock = new ReentrantReadWriteLock();
private final Lock readLock = lock.readLock();
private final Lock writeLock = lock.writeLock();
2. The get() method:
readLock.lock(); try { return cache.get(key); } finally { readLock.unlock(); }
- Why readLock.lock() in try, not before: ensures unlock in finally even if lock() throws
3. The put() method:
writeLock.lock(); try { cache.put(key, value); } finally { writeLock.unlock(); }
4. The cache-aside pattern (get-or-load):
- Double-checked locking with read lock first, upgrade to write lock only on miss
- Why read-then-write without re-checking is a TOCTOU race — show the broken version
- Show the correct pattern: release read lock, acquire write lock, re-check before loading
5. Background refresh with a ScheduledExecutorService:
- Refresh runs: writeLock.lock() → load from DB → cache.clear() → cache.putAll(fresh) → writeLock.unlock()
- Readers are blocked only during the bulk replace, not during the DB fetch
- Optimization: load into a new Map, then swap the reference under write lock (shorter hold time)
6. StampedLock as an alternative to ReadWriteLock:
- tryOptimisticRead() — reads without acquiring a lock, validates after
- Validate with stamp: if (!lock.validate(stamp)) acquire a full read lock
- When StampedLock is better than ReadWriteLock (very high contention, short critical sections)
7. A concurrent benchmark test that verifies:
- 100 reader threads get correct results while 1 writer thread updates every 100ms
- No ConcurrentModificationException or stale reads"
Part 4: Debugging Prompts (Prompts 15–19)
Debugging concurrency bugs requires different inputs than implementation prompts. Always paste the actual diagnostic artefact — the thread dump, the stack trace, the GC log — not a description of it. The AI’s diagnostic accuracy drops significantly when working from summaries.
Prompt 15: Diagnose Thread Dump Contention
When to use: The application is unresponsive, slow, or spiking in CPU/latency under load and you have collected a thread dump. This is the single most valuable diagnostic tool for Java concurrency problems.
"Analyse the following Java thread dump and identify all concurrency contention issues.
Thread dump (from jstack, kill -3, or VisualVM — paste the COMPLETE dump):
[PASTE THE FULL THREAD DUMP HERE — include ALL threads, not just the interesting ones]
Application context:
- Framework: [e.g. Spring Boot 3.x / Tomcat / Netty]
- Thread pool sizes: [e.g. Tomcat max-threads=200, HikariCP pool=20, custom-pool=50]
- Observed symptom: [e.g. p99 latency jumped from 50ms to 5s, CPU at 0%, all requests timing out]
- When the problem started: [e.g. after a deployment, under a specific load pattern]
Analysis to perform:
1. Thread state summary:
- Count threads by state: RUNNABLE / BLOCKED / WAITING / TIMED_WAITING
- A high BLOCKED count = lock contention (something is holding a lock too long)
- All threads WAITING on HikariCP = database connection pool exhaustion
- All threads WAITING on LinkedBlockingQueue = thread pool queue full (producer faster than consumer)
2. Lock contention graph:
- Find all 'waiting to lock ' entries
- Match each lock address to the thread that 'locked '
- Build the wait graph: Thread A waits for Thread B which holds Lock X
- Identify the lock HOLDER: what is it doing? Is it also blocked?
3. Deadlock detection:
- Look for cycles: Thread A waits for Thread B which waits for Thread A
- The JVM marks deadlocks as 'Found one Java-level deadlock' — check for this section
- Show the exact lock acquisition order that caused the deadlock
4. Common patterns to identify:
- HikariCP exhaustion: all threads at HikariDataSource.getConnection() — pool too small or connection leak
- Tomcat thread exhaustion: all threads blocked on downstream HTTP calls — add circuit breaker
- AsyncAppender queue full: all threads blocked at log.info() — log appender is the bottleneck
- synchronized + blocking I/O: a thread holds a monitor while doing I/O — pinning pattern
5. Recommended fixes ranked by impact:
For each finding: component, root cause, fix, and urgency (CRITICAL / HIGH / LOW)"
Prompt 16: Find a CompletableFuture That Never Completes
When to use: An async operation hangs — requests never get a response, the future never resolves — but there is no exception and no timeout fires. This is one of the hardest concurrency bugs to diagnose because it leaves no trace.
"Debug the following CompletableFuture chain that hangs and never completes.
Code that hangs:
[PASTE THE CompletableFuture CODE THAT NEVER RESOLVES]
Symptoms:
- [e.g. The endpoint returns no response and eventually times out at the gateway level]
- [e.g. The issue is intermittent — happens 1 in 100 requests]
- [e.g. No exception is thrown, no timeout fires]
Thread dump from a hanging state (if available):
[PASTE THREAD DUMP OR DESCRIBE WHAT YOU SEE]
Debug checklist to work through:
1. Missing completion path:
- Is there ANY code path where the CompletableFuture is never completed?
- Check: if (condition) { future.complete(x); } — what happens in the else branch?
- Check: CompletableFuture that is created and returned but the task that was supposed to
complete it was never submitted (e.g., executor was shut down before submission)
2. Exception swallowing:
- Is there a bare try-catch inside a supplyAsync lambda that catches the exception
and does NOT call future.completeExceptionally()?
- This creates a future that is neither complete nor exceptionally complete — it hangs forever
3. Callback on wrong executor:
- Is the completion callback registered on a dead or shut-down executor?
- Check: executor.shutdown() called before the callback can run
4. join() or get() inside a thenApply() callback:
- This creates a deadlock in the ForkJoinPool: the thread that should run the callback
is blocked waiting for a future that needs that same thread to complete
- grep for .join() or .get() inside thenApply / thenCompose lambdas
5. allOf() with a future that was never submitted:
- CompletableFuture.allOf(f1, f2, f3) where f3 is a new CompletableFuture() never completed
6. How to add hang detection:
- orTimeout(30, TimeUnit.SECONDS) on the outermost future
- whenComplete logging that fires even on hang (use a watchdog ScheduledExecutorService)
- JFR event: jdk.VirtualThreadPinned or jdk.ThreadSleep to find the blocked location"
Prompt 17: Identify Thread Pool Exhaustion
When to use: Under load, all requests start timing out simultaneously, latency spikes from milliseconds to tens of seconds, and throughput collapses. This is the signature of thread pool exhaustion — the most common Java concurrency production incident.
"Diagnose and fix thread pool exhaustion in the following service.
Evidence of exhaustion (paste whatever you have):
- Application logs: [PASTE LOG LINES showing queue full, rejection, or timeout errors]
- Metrics showing the spike: [e.g. active_threads=200/200, queue_depth=500/500, p99=30s]
- Thread dump (if collected during incident): [PASTE OR DESCRIBE]
Service configuration:
[PASTE your ThreadPoolExecutor or application.yml thread pool settings]
Code that submits to the thread pool:
[PASTE the service methods that use the pool]
Diagnosis steps:
1. Identify the exhaustion point:
- Is the pool at max threads AND the queue full? → both pool and queue are undersized, or tasks are slow
- Is the pool at max threads but queue is empty? → tasks are hanging (see Prompt 16)
- Is the pool at max threads with fast task completion? → max size is too small for the load
2. Find the slow task:
- What operation does each pooled task perform?
- How long does each task take under normal conditions vs during the incident?
- Is there a downstream dependency that became slow (DB, external API) that caused task duration to spike?
3. Connection between downstream latency and thread pool:
- If each task waits 5s for a slow DB and the pool has 200 threads → pool saturates at 200/5 = 40 req/s
- At 100 req/s with 5s downstream latency, you need 500 threads — show this calculation
4. Fixes (multiple options — recommend based on root cause):
Option A: Add a circuit breaker — stop submitting tasks when downstream is slow (Resilience4j @CircuitBreaker)
Option B: Add per-task timeout — tasks that exceed 2s fail fast instead of holding a thread for 30s
Option C: Migrate to virtual threads — for I/O-bound tasks, virtual threads eliminate pool sizing math
Option D: Increase pool size — only if root cause is genuinely insufficient threads, not slow downstream
5. Prevention: Micrometer metrics to add BEFORE the next incident:
executor.active / executor.pool.max → alert when ratio > 0.8
executor.queue.remaining / executor.queue.size → alert when queue is 80% full"
Prompt 18: Detect Race Conditions and Visibility Bugs
When to use: A bug is intermittent, impossible to reproduce in single-threaded tests, and produces wrong results or NullPointerException/ConcurrentModificationException only under concurrent load. These are the hardest bugs to find because they require holding the whole object graph in your head while reasoning about interleaving.
"Detect all race conditions and visibility bugs in the following class that is shared between threads.
Class to audit:
[PASTE THE COMPLETE CLASS HERE]
Concurrency context:
- How is this class deployed: [e.g. Spring @Service singleton, shared between all HTTP request threads]
- Known symptom: [e.g. intermittent NullPointerException on line 47, 1 in 1,000 requests]
Audit checklist:
1. Mutable shared state without synchronization:
- Non-final fields that are read and written from multiple threads without volatile or synchronized
- Collections (HashMap, ArrayList, LinkedList) modified from multiple threads
- Fix: ConcurrentHashMap, CopyOnWriteArrayList, or explicit ReentrantReadWriteLock
2. Check-then-act race (TOCTOU):
- if (!map.containsKey(key)) { map.put(key, value); }
→ Another thread can insert between the check and the put
- Fix: map.putIfAbsent(key, value) or computeIfAbsent()
3. Compound operations on atomic variables:
- if (counter.get() > 0) { counter.decrementAndGet(); }
→ Another thread can decrement between get() and decrementAndGet()
- Fix: counter.updateAndGet(v -> v > 0 ? v - 1 : 0)
4. Visibility hazards (memory visibility, not just atomicity):
- A field written by Thread A may not be visible to Thread B without happens-before
- Non-volatile boolean flags used as stop signals: while (running) { ... }
- Fix: volatile boolean running or AtomicBoolean
5. Broken double-checked locking:
- if (instance == null) { synchronized (this) { if (instance == null) { instance = new X(); } } }
Without volatile on instance, the partially-constructed object can be visible
- Fix: add volatile to the field declaration
6. Mutable objects returned from getters:
- getItems() returns the internal list directly → callers can modify it
- Fix: return Collections.unmodifiableList(items) or a defensive copy
For each finding: the exact lines involved, the specific interleaving that triggers the bug, and the corrected code."
Prompt 19: Find Deadlocks in a Running JVM
When to use: The application has completely frozen — no requests are being processed, CPU is at 0%, and the process is alive but unresponsive. This is the signature of a deadlock. Unlike other concurrency bugs, deadlocks are loud and reproducible.
"Diagnose and fix the following Java deadlock.
Evidence (paste ALL that you have):
Option A — JVM deadlock report from jstack (look for 'Found one Java-level deadlock:'):
[PASTE THE DEADLOCK SECTION FROM jstack PID]
Option B — Thread dump showing BLOCKED threads:
[PASTE THE RELEVANT BLOCKED THREAD ENTRIES]
Option C — The code you suspect contains the deadlock:
[PASTE THE CLASSES WITH SYNCHRONIZED METHODS / LOCK ACQUISITION CALLS]
Deadlock analysis:
1. Read the lock graph:
- Thread A holds Lock X and waits for Lock Y
- Thread B holds Lock Y and waits for Lock X
- Show the cycle from the thread dump
2. Find the lock acquisition order in the code:
- Show the two code paths that acquire the same locks in OPPOSITE orders
- This is always the root cause: inconsistent lock ordering
3. Fixes (in order of preference):
Option A: Consistent lock ordering (best — eliminates the deadlock structurally)
- Always acquire locks in the same order everywhere in the codebase
- Use System.identityHashCode() to define a canonical ordering for dynamic lock sets:
if (System.identityHashCode(lockA) < System.identityHashCode(lockB))
{ acquire lockA first, then lockB }
Option B: tryLock with timeout (defensive)
lock.tryLock(100, TimeUnit.MILLISECONDS) — fail fast instead of waiting forever
Log a WARN if tryLock() returns false — this indicates contention
Option C: Lock elimination — can you redesign to remove the need for one of the locks?
(e.g. use a concurrent data structure instead of synchronized access)
4. Deadlock detection at runtime:
- ThreadMXBean.findDeadlockedThreads() — call this from a monitoring thread every 30s
- Log a CRITICAL alert with the deadlocked thread names when detected
- Show the JMX-based monitoring code
5. Unit test that provably reproduces the deadlock:
- Use two threads and a CountDownLatch to force the interleaving that triggers it
- Verify the fix prevents deadlock under the same interleaving"
Part 5: Full Concurrency Audit (Prompt 20)
Prompt 20: Full Concurrency Audit of a Service Class
When to use: Before a production release, a major refactoring, or when onboarding to a service you are taking ownership of. This prompt produces a comprehensive concurrency quality report covering every dimension: thread safety, pool configuration, CompletableFuture correctness, virtual thread readiness, and observability.
"Perform a comprehensive concurrency audit of the following service class and produce a prioritised finding report.
Class to audit (paste complete class including all fields, constructors, and methods):
[PASTE THE CLASS HERE]
Supporting context:
- How it is used: [e.g. Spring @Service singleton, called by 400 concurrent HTTP request threads]
- Java version: [e.g. 21]
- Spring Boot version: [e.g. 3.4]
- Known performance issues: [e.g. p99 spikes every 5 minutes when batch job runs]
- Known intermittent bugs: [e.g. occasional NullPointerException in production, no repro in dev]
Audit dimensions — evaluate EVERY one:
THREAD SAFETY:
[ ] Mutable fields accessed from multiple threads without synchronization?
[ ] Collections (HashMap, ArrayList) used as shared state without ConcurrentHashMap / locking?
[ ] Static mutable state that persists between requests?
[ ] @NotThreadSafe types (SimpleDateFormat, Calendar, Random) used as shared fields?
[ ] Singleton state being mutated during request processing?
CompletableFuture CORRECTNESS:
[ ] Blocking calls (get(), join(), sleep()) inside thenApply() or thenCompose() lambdas?
[ ] Missing exceptionally() or handle() — exceptions that will be silently swallowed?
[ ] thenApplyAsync() used where thenApply() suffices (unnecessary thread switch)?
[ ] thenCompose() used where thenApply() should be used (wrapping non-future returns in futures)?
[ ] Missing timeout on any CompletableFuture that calls an external system?
[ ] allOf() where anyOf() would be more appropriate (or vice versa)?
THREAD POOL USAGE:
[ ] Blocking I/O in the ForkJoinPool common pool (default for CompletableFuture.supplyAsync())?
[ ] Single shared pool for workloads with different latency profiles (bulkhead missing)?
[ ] Pool sized with a hardcoded number with no calculation or comment?
[ ] No rejection handler — default AbortPolicy will throw without logging?
[ ] No queue depth monitoring — pool saturation is invisible?
VIRTUAL THREAD READINESS (Java 21+):
[ ] synchronized blocks wrapping blocking calls (carrier pinning on Java 21–24)?
[ ] Large ThreadLocal values that create memory pressure at scale?
[ ] I/O-bound workload that would benefit from spring.threads.virtual.enabled=true?
OBSERVABILITY:
[ ] No metrics on thread pool utilisation?
[ ] No timing on async operation durations?
[ ] No tracing context propagation through CompletableFuture chains?
Output format for each finding:
1. Category: [Thread Safety / CF Correctness / Pool Usage / Virtual Threads / Observability]
2. Severity: CRITICAL (data corruption / deadlock) / HIGH (production failure) / MEDIUM (performance) / LOW (maintainability)
3. Location: method name + approximate line
4. Description: what is wrong and what will happen under concurrent load
5. Fix: corrected code
6. Test: how to write a test that proves the fix works
Summary at the end:
- Total findings by severity
- Top 3 highest-priority fixes
- Estimated effort to remediate all findings"
Tips for Getting the Best Results
- For debugging prompts (15–19), always paste the actual artefact. A thread dump is 500 lines. Paste all 500. An AI analysing “my threads are blocked” produces generic advice; an AI reading the actual
locked <0x00000006c0a54b10>addresses produces the specific lock and the exact thread holding it. - For implementation prompts (1–14), fill in the numbers. Thread pool sizing, latency budgets, concurrency targets — these are not boilerplate. An executor sized for 500 req/s at 200ms/task has a very different pool size than one sized for 50 req/s at 2000ms/task. The formula is in Prompt 1; use it.
- The counterexample matters. Prompt 11 explicitly asks for a benchmark proving virtual threads do NOT help for CPU-bound work. Include this when presenting a virtual thread migration to your team — it shows the analysis is honest and builds more trust than a one-sided case.
- Prompt 3 is the most commonly misused. The thenApply vs thenCompose distinction trips up everyone. Run Prompt 3 on every non-trivial CompletableFuture chain before code review. The incorrect operator does not always cause an exception — sometimes it just runs on the wrong thread silently.
- Deadlock bugs are reproducible; use Prompt 19 first. If the application is completely frozen and CPU is at 0%, it is almost certainly a deadlock. Run
jstack <pid>immediately — the JVM’s deadlock detector will mark the cycle for you. Prompt 19 decodes it.
Frequently Asked Questions
When should I use CompletableFuture vs virtual threads vs reactive (WebFlux)?
Use virtual threads (Java 21+, spring.threads.virtual.enabled=true) for services that are primarily I/O-bound and use a blocking programming model — most Spring Boot REST APIs fall here. Use CompletableFuture when you need explicit fan-out/fan-in, timeout composition, or conditional async chaining within a service method that is otherwise imperative. Use reactive / WebFlux when the entire stack is reactive from the client to the database, or when you need backpressure propagation across a streaming pipeline. The three approaches are not mutually exclusive: virtual threads + CompletableFuture for fan-out is a natural combination. See the Virtual Threads vs WebFlux benchmark for measured comparisons.
What is the difference between thenApply, thenApplyAsync, thenCompose, and thenComposeAsync?
thenApply(fn) transforms the result synchronously — the function runs on whatever thread completed the upstream future. thenApplyAsync(fn) schedules the function on the ForkJoinPool (or a specified executor) regardless of which thread completed the upstream. thenCompose(fn) is the flat-map equivalent: use it when your transform function itself returns a CompletableFuture — it unwraps the nested future. Using thenApply where thenCompose is needed wraps the inner future in an outer one, giving you a CompletableFuture<CompletableFuture<T>> instead of CompletableFuture<T>. The Async suffix on thenComposeAsync controls the thread switch for the compose function itself, not for the inner future.
Is it safe to call CompletableFuture.join() inside a thenApply() callback?
No — this is one of the most common CompletableFuture bugs. If the callback runs on the ForkJoinPool common pool and the future being joined also needs a thread from that pool to complete, the callback thread is blocked waiting for a thread that it is occupying. This creates starvation that looks like a deadlock. Always chain with thenCompose() instead of nesting join() calls inside callbacks. Prompt 3 covers this in detail.
How do I propagate tracing context (Micrometer / OpenTelemetry) through CompletableFuture chains?
Spring Boot 3.x + Micrometer Tracing propagates trace context automatically through @Async methods and through WebClient/RestClient. For manual CompletableFuture chains, you need to capture the current Observation context before submitting the async task and restore it inside the lambda. Micrometer’s ContextSnapshot wraps this pattern: ContextSnapshot.setThreadLocalsFrom(context) inside your supplyAsync lambda. Without this, spans from async work will not be children of the incoming request span, and distributed traces will appear broken in Zipkin/Grafana Tempo.
Should I use ForkJoinPool.commonPool() or a dedicated executor?
Avoid the common pool for production workloads. CompletableFuture.supplyAsync(task) without an explicit executor uses the ForkJoinPool common pool, which is shared across the JVM — including third-party libraries, parallel streams, and any other code that uses it. A slow or blocking task submitted there starves every other user of the pool. Always pass an explicit executor: CompletableFuture.supplyAsync(task, myDedicatedExecutor). Reserve the common pool only for short, CPU-bound, non-blocking computations with predictable duration.
How do I test concurrent code reliably?
The most reliable technique is to use a CountDownLatch to force the exact interleaving that triggers the bug, run the scenario with many threads, and verify the outcome. For race condition detection, consider the JCStress harness — it is purpose-built for Java concurrency correctness testing and can exhaustively test interleaving scenarios that are impossible to hit reliably with a standard JUnit test. For thread safety audits, Prompts 18 and 20 work best when combined with a review pass using Java Thread Sanitizer (available via async-profiler or JFR).
See Also
- Java Concurrency Deep Dive: CompletableFuture, ExecutorService, ForkJoinPool
- Virtual Threads vs Platform Threads: Real Benchmarks and When to Use Which
- Virtual Threads vs Reactive (WebFlux) vs Platform Threads in Spring Boot 3.4
- 10 AI Prompts for Java Performance Optimization
- 10 AI Prompts to Review and Improve Java Code Quality
- 10 AI Prompts for Designing Java Microservices
- Java 25 LTS: Every JEP That Matters (with AI Prompts for Each Migration)
- Java 21 to Java 25 LTS: Every Feature You Actually Need to Know
Conclusion
These 20 prompts cover the full spectrum of Java concurrency work: sizing executors correctly from the start (Prompt 1), building correct and composable async pipelines (Prompts 2–5), applying production-hardened patterns like bulkheads, rate limiting, and structured concurrency (Prompts 6–14), diagnosing the five most common production concurrency incidents (Prompts 15–19), and auditing an entire service before it ships (Prompt 20). The implementation prompts work best when you fill in the numbers from your actual workload. The debugging prompts work best when you paste the actual diagnostic artefact. The combination of your domain knowledge and the AI’s fluency with the concurrency API produces faster, more accurate results than either approach alone — particularly for the subtle bugs that only manifest under specific thread interleavings that are hard to reason about without a tool that can hold the entire state space in mind at once.