Wondering why Java applications struggle to scale with thousands of concurrent tasks?
The answer lies in the cost of using traditional Java threads, known as platform threads. In this blog post, we’ll explore how platform threads behave under load, the limitations they pose, and why developers are shifting to Java’s new virtual threads for scalability.
Platform threads in Java are heavyweight OS threads that consume a significant amount of memory (typically 1MB stack each), making them expensive to create and manage at scale. Beyond a certain threshold, the JVM throws errors like OutOfMemoryError: unable to create new native thread.
๐งต What Are Platform Threads in Java?
A platform thread is Java’s traditional thread backed by a native operating system thread. Each thread:
- Is created using the new Thread() API
- Requires dedicated stack memory (~1MB by default)
- Is limited by OS-level thread creation limits
๐ก Key Characteristics of Platform Threads:
| Feature | Platform Thread |
| ------------------- | ------------------------------------- |
| Backed By | OS native thread (e.g., POSIX thread) |
| Stack Memory | \~1MB (adjustable with `-Xss`) |
| Creation Cost | High |
| Scalability | Limited |
| Blocking Operations | Blocks the OS thread |
๐ฏ Why Understanding Thread Creation Cost Matters
In microservice architectures, Java apps make frequent network calls and I/O-bound operations. These operations often block threads, making them idle while waiting for responses.
To handle high concurrency, developers often try to create more threads — but platform threads come at a cost:
- High memory usage per thread
- OS-imposed limits on number of threads
- Performance bottlenecks under load
Let’s explore this with a practical example.
๐ ️ Java Thread Creation Demo – Hands-On
We'll simulate a typical I/O-heavy workload using Thread.sleep() to mimic latency, and then create thousands of threads to see what happens.
✅ Project Setup
- Java Version: 21 (for Duration.ofSeconds)
- Tools: IntelliJ IDEA or any Java IDE
๐งฑ Simulate I/O With Thread.sleep()
public class Task {
private static final Logger logger = LogManager.getLogger(Task.class);
public static void ioIntensive(int i) {
logger.info("Starting IO Task: " + i + " - " + Thread.currentThread().getName());
try {
Thread.sleep(Duration.ofSeconds(10));
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
logger.info("Ending IO Task: " + i + " - " + Thread.currentThread().getName());
}
}
๐ Why Thread.sleep()?
Though it’s not a real network call, it effectively simulates thread blocking, allowing us to observe memory and OS behavior without external dependencies.
๐ Creating Threads in a Loop
public class InboundOutboundTaskDemo {
private static final int MAX_PLATFORM_THREADS = 50_000;
public static void main(String[] args) {
for (int i = 0; i < MAX_PLATFORM_THREADS; i++) {
final int taskNumber = i;
Thread thread = new Thread(() -> Task.ioIntensive(taskNumber));
thread.start();
}
}
}
Run the code with MAX_PLATFORM_THREADS = 10 — it works fine.
Increase it to 50,000, and you’ll likely see:
Exception in thread "main" java.lang.OutOfMemoryError: unable to create new native thread
๐ Why Does Thread Creation Fail?
Let’s break it down.
๐ Behind the Scenes
- When you create a thread:
- JVM uses native OS call pthread_create() (on Linux/macOS)
- Each thread gets a default stack of ~1MB
- OS imposes a per-process limit on thread count
Even if you have available heap memory, thread creation can still fail due to:
- Native memory exhaustion
- Reaching user-level thread limits
๐ Java Platform Thread Limits: A Quick Comparison
| Resource | Platform Thread | Virtual Thread (Preview) | | ----------------------- | --------------- | ------------------------ | | Memory per Thread | \~1MB | Few KB | | Max Threads in Practice | \~8,000–15,000 | 1,000,000+ | | Blocking Cost | High | Minimal (non-blocking) | | Backed by OS Thread? | Yes | No (uses carrier thread) | | Thread Creation Speed | Slow | Fast |
๐ Tuning Tips: Can We Create More Platform Threads?
Yes, but with caveats.
✅ Use JVM Flags
java -Xss256k InboundOutboundTaskDemo
This reduces thread stack size to 256KB (vs 1MB default), allowing more threads.
✅ Use Thread Pools
Instead of raw new Thread(), prefer:
ExecutorService executor = Executors.newFixedThreadPool(200);
But even with thread pools, you’re limited by blocked threads during I/O calls.
๐ง Alternative: Enter Virtual Threads (Project Loom)
Java 21 introduced virtual threads as a preview feature. These threads are:
- Lightweight
- Not backed by OS threads
- Scalable to millions of concurrent tasks
Stay tuned — in the next post, we’ll rewrite this same example using virtual threads and compare memory usage, performance, and thread behavior.
✅ Pros and Cons of Java Platform Threads
๐ Pros:
Mature and battle-tested
Great for CPU-bound tasks
Full integration with debuggers, profilers, and logging
๐ Cons:
High memory usage per thread
Not suitable for high-concurrency I/O apps
Blocking operations are expensive
๐ Summary: Should You Still Use Platform Threads?
Use platform threads when:
- You’re doing CPU-bound tasks
- The number of concurrent threads is low to moderate
- You need deep integration with existing thread tools
Avoid platform threads when:
- You're building a high-throughput microservice
- The app makes lots of blocking I/O calls
- You need to handle hundreds of thousands of concurrent users