Java Concurrency & Async Programming · Lesson 24

Java Concurrency Budget Lab: Queues, Virtual Threads, and Cancellation

Run six deterministic Java concurrency scenarios to compare fixed pools, virtual threads, resource guards, deadlines, and cooperative cancellation.

The Java Concurrency Budget Lab is a deterministic discrete-event model. It creates no Java threads, runs no benchmark, contacts no service, and uploads no input. Identical valid inputs produce the same ordered ticks, counters, boundary statement, recommendations, and copyable summary.

Quick answer

Use the six presets to separate executor admission from resource capacity. A fixed pool queues work after its workers are occupied. Virtual threads remove that modeled executor queue, but resource permits still control useful progress. Without a guard, demand can exceed resource capacity. A deadline happens before work advances at that tick; cooperative tasks cancel and release capacity, while tasks that ignore interrupt continue after the caller has timed out.

Learning objectives

  • Predict accepted, rejected, queued, started, resource-waiting, completed, timed-out, and canceled work from the lab’s fixed tick rules.
  • Explain why virtual threads, caller deadlines, and future cancellation do not add downstream capacity or prove underlying side effects stopped.

Prerequisites

Read ExecutorService, Thread Pools, and Work Queues, Java Virtual Threads, and Java Interruption and Deadlines. The tutorial closes the Java Concurrency course and appears in the topic cluster.

How the deterministic model works

All tasks arrive at tick 0 in task-ID order. A fixed pool accepts at most workers plus queue capacity; extra tasks reject immediately. Virtual-thread-per-task accepts all tasks and starts them at tick 1, so worker and queue inputs are visibly not applied. Blocking work needs two progress ticks; CPU work needs three.

A resource guard limits which started tasks may demand progress. Without a guard, every started task demands the resource, but only the first resourceCapacity tasks progress in a tick. Excess demand is recorded as contention and a capacity breach; the model does not invent a downstream error, slowdown, or crash. At the deadline tick, interruption occurs before work progress. Cooperative tasks cancel; ignored interruption leaves tasks running.

These rules teach boundaries. A tick is not a millisecond, task service times are not measurements, and the results are not production configuration advice.

Preset 1: Fixed-pool queue convoy

Load Fixed-pool queue convoy. Eight blocking tasks are accepted because two workers plus six queue slots equal eight. Only two start initially. They retain their workers while using their two guarded resource permits, and later tasks wait in FIFO order. The queue is admitted waiting, not extra processing capacity.

In a real JVM, durations vary and task mix can change ordering. Validate an executor using active workers, queue depth and age, rejection, dependency waits, and user latency. Do not copy the teaching values into Spring.

Preset 2: Virtual threads, same downstream bottleneck

All eight tasks start on modeled virtual threads at tick 1. Peak executor queue depth and platform-worker use are reported as zero because those fixed-pool controls do not apply. Yet only two guarded tasks can progress. The executor bottleneck disappeared; the downstream capacity did not.

This is why Virtual Threads for Backend Services recommends thread-per-task execution plus a separate semaphore, bulkhead, or admission control for scarce resources.

Preset 3: CPU-bound work does not become faster

Six CPU-bound tasks start, need three progress ticks, and share two modeled resource lanes. Virtual threads do not create CPU cores. The lab deliberately uses the same resource mechanism for teaching; a production CPU budget must come from processor saturation, runnable work, service-level objectives, and representative profiling.

Preset 4: Cooperative deadline cancellation

Six blocking tasks share one guarded permit and a deadline at tick 3. The deadline event appears before any tick-3 progress. Every unfinished task receives an interrupt, observes it, enters canceled, and releases modeled worker or guard capacity. This demonstrates the desired local contract, not a JVM guarantee that arbitrary code cooperates.

Verify real cancellation while queued, waiting for a permit, blocked in each dependency client, and executing application code. Always release permits and locks in finally.

Preset 5: Ignored interrupt continues after deadline

Change only interruption handling to ignored. The request is timed out, but unfinished tasks emit continued and eventually count under completedAfterDeadline. Caller timeout and underlying completion are now visibly different facts.

If this work charges a payment or sends an email, interruption cannot undo the effect. Use stable operation identity, idempotency, status lookup, and reconciliation. CompletableFuture.cancel() likewise does not reliably stop arbitrary underlying work, as the CompletableFuture guide explains.

Preset 6: Virtual threads without a resource guard

All eight tasks demand a resource with capacity two. Two progress; the rest record resource-contended. Peak demand exceeds capacity and breach ticks are counted. The lab intentionally refuses to claim the downstream must fail or become slower. That outcome belongs to the real resource and must be measured.

Add a guard to make admission match a known capacity contract. In production, choose the limit from load evidence and coordinate it with connection pools, retries, deadlines, and other callers.

Reading the summary safely

accepted and rejected describe executor admission. started describes tasks that reached execution. timedOut counts unfinished tasks at the deadline. canceled means the cooperative teaching branch stopped them. completedAfterDeadline exposes the ignored-interrupt boundary. Peak resource demand is not throughput, and a capacity-breach tick is not a predicted outage.

Copy the text to discuss a scenario in review, then reset and change one field. Use Ctrl+Enter or Command+Enter to run. The tool validates every numeric range and never permits infinite retries or unbounded simulation.

Production validation

Move from the model to Production Java Concurrency Troubleshooting. Capture request outcome, active and queued work, queue age, rejection, CPU, connection acquisition, downstream concurrency, locks, timeouts, cancellation observed, and work completed after deadline. Use JFR and jcmd for JVM evidence. Reconcile durable state separately. A lab result is never a benchmark, capacity plan, or deployable Spring configuration.

FAQ

Does the lab create real virtual threads?

No. It is a deterministic TypeScript state machine in the browser and intentionally avoids benchmarking.

Why are worker and queue controls disabled for virtual threads?

The model uses virtual-thread-per-task execution, so fixed platform-worker and executor-queue limits are not applied. Scarce resources remain separately guarded.

Does a capacity breach mean the downstream crashes?

No. It means modeled active demand exceeded modeled capacity. The real response could include waiting, throttling, errors, slowdown, or another behavior.

Can I use the results as production settings?

No. Use measured workload, JVM, database, dependency, and user evidence with a controlled rollout.

Return to the Java Concurrency learning path, browse the Java Concurrency topic, and compare system-level controls in Load Shedding and Adaptive Concurrency Limits.

Official sources

Knowledge check

Check your understanding

Answer both questions correctly to mark this lesson as mastered. You can retry without penalty.

1. What changes between the fixed-pool and virtual-thread downstream-bottleneck presets?

2. What does a no-guard capacity-breach tick predict about the real downstream?