Real-World Java Concurrency & Parallelism Challenges: Solutions with Virtual Threads & Rate Limiting

Dev Eficiente

Summary:

This video addresses complex concurrency and parallelism challenges encountered in real-world Java applications, using a high-scale order processing system as a case study.

  • The speaker introduces fundamental concepts: sequential, parallel, and concurrent processing, synchronous vs. asynchronous operations, and CPU-bound vs. IO-bound tasks.
    Diagram illustrating sequential, parallel, and concurrent processing.
    Diagram illustrating sequential, parallel, and concurrent processing. [ 00:12:40 ]
  • The core problem involves processing millions of orders and products, generating 270,000 requests per second, which leads to performance issues and Out-Of-Memory (OOM) errors.
    Table summarizing the large-scale order processing problem.
    Table summarizing the large-scale order processing problem. [ 00:19:20 ]
  • Initial attempts with sequential processing were too slow, while parallel streams led to issues like nested streams and lost thread context.
    Diagram illustrating the issue of nested parallel streams overloading shared CPU cores.
    Diagram illustrating the issue of nested parallel streams overloading shared CPU cores. [ 00:38:39 ]
  • Moving to IO-bound solutions with Executors and CompletableFuture exposed OOM errors due to shared thread pools, necessitating pool separation and rate limiting for dependencies.
    Graph illustrating a memory spike leading to an Out-of-Memory (OOM) error.
    Graph illustrating a memory spike leading to an Out-of-Memory (OOM) error. [ 00:59:49 ]
  • Virtual Threads, introduced as a lighter alternative, also presented challenges like "pinning" and further OOMs when replacing traditional threads without careful resource management.
    Diagram illustrating how virtual threads are mapped to platform threads.
    Diagram illustrating how virtual threads are mapped to platform threads. [ 01:26:58 ]
  • The most effective solution combined virtual threads with semaphores to control backpressure, achieving optimal performance.
    Performance metrics for the simplified Virtual Threads solution, showing reduced peak RPS.
    Performance metrics for the simplified Virtual Threads solution, showing reduced peak RPS. [ 01:37:38 ]
  • Key takeaways emphasize the importance of experimentation, measurement, protecting resources (especially external dependencies), and prioritizing simplicity in concurrency solutions.
    Key takeaways for implementing concurrency solutions.
    Key takeaways for implementing concurrency solutions. [ 01:38:18 ]

Introduction and Speaker's Background [0:00:00]

Concurrency and Parallelism Fundamentals [0:11:42]

The Real-World Problem: Orders Processing at Scale [0:19:15]

Initial Solutions and Their Pitfalls [0:21:55]

Sequential Solution [0:21:55]

CPU-Bound Solution: Parallel Streams [0:24:01]

Diagram illustrating the issue of nested parallel streams overloading shared CPU cores.
Diagram illustrating the issue of nested parallel streams overloading shared CPU cores. [ 00:38:39 ]
* Solution #1: Parallelize where it matters most: Apply parallel streams only at the highest level where there are large, independent data sets (e.g., raw data processing), avoiding nesting. [0:39:14]
Code snippet showing the optimized parallel stream application at the highest data level.
Code snippet showing the optimized parallel stream application at the highest data level. [ 00:40:20 ]
* Problem #2: A Flatmap Surprise: Using parallelStream() within a flatMap operation surprisingly results in sequential execution due to flatMap's internal implementation transforming the stream to sequential. [0:40:29]
Code snippet demonstrating a parallel stream unexpectedly running sequentially within a `flatMap`.
Code snippet demonstrating a parallel stream unexpectedly running sequentially within a `flatMap`. [ 00:40:29 ]
* Solution #2: Reverse the order: Move the parallelStream() call to an outer level of iteration to ensure parallelism is applied where intended. [0:41:43] * Problem #3: Parallel Stream & Thread Context: Parallel streams execute tasks in different threads from the commonPool, meaning they lose access to the original thread's context (e.g., user transaction context). This makes them unsuitable for many server-side applications that rely on thread-local contexts. [0:42:42]
Code snippet highlighting how parallel streams lose the original thread's context.
Code snippet highlighting how parallel streams lose the original thread's context. [ 00:42:49 ]

IO-Bound Solution: Executors & CompletableFuture [0:53:11]

Diagram explaining how a shared thread pool leads to OOM due to accumulating large response objects.
Diagram explaining how a shared thread pool leads to OOM due to accumulating large response objects. [ 01:00:49 ]
* Solution #4: Separate the pools: Use different ExecutorService instances for different responsibilities (e.g., one for initiating requests, another for processing responses), ensuring that large, incoming data doesn't block the main processing queue. thenApply (faster, no context switch) and thenApplyAsync (allows specifying executor) were discussed for processing CompletableFuture results. [1:02:01]
Diagram showing how separating thread pools mitigates Out-of-Memory issues.
Diagram showing how separating thread pools mitigates Out-of-Memory issues. [ 01:02:09 ]
Table comparing `thenApply` and `thenApplyAsync` performance, flexibility, and context preservation.
Table comparing `thenApply` and `thenApplyAsync` performance, flexibility, and context preservation. [ 01:05:19 ]

Managing Load: Self-Throttling [1:12:11]

The Latest Innovation: Virtual Threads [1:25:55]

Understanding Virtual Threads [1:25:55]

Problem #7: The Pinning Problem [1:29:05]

Problem #8: Yet Another Out Of Memory [1:31:18]

Solution #8: Back to the Semaphore [1:32:23]

Performance Results and Key Takeaways [1:33:28]