Java Threads — Why Direct Thread Creation Fails at Scale
Each Java thread allocates ~1MB; at 2000 req/s the JVM exhausts native memory before GC.
- Threads are lightweight units of execution within a JVM process
- Runnable is a functional interface that decouples task logic from thread management
- Prefer Runnable over extending Thread for flexibility and reusability
- Creating threads manually per request causes OOM under load — always use a pool
- Biggest mistake: calling run() instead of start() — that's just a method call on the current thread
Think of Java Threads and Runnable Explained as a powerful tool in your developer toolkit. Once you understand what it does and when to reach for it, everything clicks into place. Imagine a restaurant kitchen: a Thread is like a physical chef, and a Runnable is the recipe card. You can have a chef who only knows one recipe (extending Thread), but it is much more flexible to have a professional chef who can pick up any recipe card you hand them (implementing Runnable). This allows your 'chefs' to stay busy with different tasks without being restricted to just one job.
Java Threads and Runnable Explained is a fundamental concept in Java development. It is the bedrock of concurrency, allowing your applications to perform multiple tasks simultaneously, such as processing a file in the background while keeping the user interface responsive. In the modern landscape of high-throughput microservices at io.thecodeforge, understanding how to manage these units of execution is the difference between a scalable system and a bottlenecked one.
In this guide, we'll break down exactly what Java Threads and Runnable Explained is, why it was designed to separate the task logic from the execution mechanism, and how to use it correctly in real projects.
By the end, you'll have both the conceptual understanding and practical code examples to use Java Threads and Runnable Explained with confidence.
What Is Java Threads and Runnable Explained and Why Does It Exist?
Java Threads and Runnable Explained is a core feature of Concurrency. It was designed to solve the problem of sequential execution bottlenecks. In a single-threaded environment, a long-running task blocks the entire application. By using the Thread class or the Runnable functional interface, developers can delegate work to independent execution paths. The Runnable interface is generally preferred because it supports the 'Composition over Inheritance' principle, allowing your class to extend another base class (like a Spring service) while still being executable by a thread. At io.thecodeforge, we treat Runnable as the blueprint and Thread as the engine.
Common Mistakes and How to Avoid Them
When learning Java Threads and Runnable Explained, most developers hit the same set of gotchas. A classic mistake is calling instead of run(). Calling start() simply executes the code in the current thread like a normal method call, whereas run() triggers the JVM to create a new call stack. Another common pitfall is 'Thread Leaks,' where threads are created but never terminated or managed by a pool, eventually exhausting system memory. In production, we almost never create threads manually; we use managed pools to recycle these expensive resources.start()
run() instead of start() is a silent no-op in terms of parallelism.run() outside of a test, flag it immediately.Thread Lifecycle and State Transitions
A Java thread goes through six states: NEW, RUNNABLE, BLOCKED, WAITING, TIMED_WAITING, TERMINATED. Understanding these states is critical for debugging hangs and performance issues. A thread enters NEW when created but not started. After , it moves to RUNNABLE (actually ready to run; the OS scheduler decides when it actually runs). When a thread tries to acquire a lock held by another thread, it enters BLOCKED. WAITING occurs when a thread calls start(), wait(), or join(). TIMED_WAITING is similar but with a timeout. Finally, TERMINATED after park() completes. One important detail: you cannot restart a thread once it reaches TERMINATED — that throws IllegalThreadStateException.run()
- NEW: Thread object created but not started. No OS thread yet.
- RUNNABLE: Ready to run or running — depends on OS scheduler.
- BLOCKED: Waiting for a monitor lock to enter a synchronized block.
- WAITING: Indefinitely waiting for another thread to notify/park_unpark.
- TIMED_WAITING: Waiting with a timeout (Thread.sleep, wait(timeout)).
- TERMINATED:
run()completed or exception thrown. Cannot be restarted.
Daemon vs User Threads: When the JVM Shuts Down
Java threads are either user threads or daemon threads. A user thread prevents the JVM from exiting until it completes. A daemon thread does not — the JVM can terminate as soon as all user threads finish. Daemon threads are ideal for background services like statistics collection or garbage collection. By default, a new thread inherits the daemon status of the creating thread; but you can set it explicitly via setDaemon(true) before calling start(). Important: trying to set daemon after start() throws IllegalThreadStateException. Also, daemon threads do not execute finally blocks on JVM exit — they are abruptly terminated.
Best Practices: From Manual Threads to ExecutorService
In modern production code, you rarely interact with Thread directly. The java.util.concurrent.ExecutorService provides a higher-level replacement: thread pool management, task submission, and future results. The typical pattern is to create a fixed thread pool sized according to the workload type (CPU-bound: nThreads = number of cores; I/O-bound: nThreads much higher). Always shut down the executor when the application stops to avoid resource leaks. Spring Boot applications can use @Async on methods with a custom AsyncConfigurer. At io.thecodeforge, we never use new Thread(...) in production code — only in tests or quick scripts.
Executors.newFixedThreadPool(nThreads) with a bounded queue and a RejectedExecutionHandler. For Spring Boot, define a TaskExecutor bean with proper pool configuration.Thread Starvation Brings Down Payment Service
new Thread(runnable).start() with an ExecutorService using a bounded pool (e.g., 50 threads) and a CallerRunsPolicy rejection handler. This prevents thread explosion and provides backpressure to the caller.- Never create threads directly in production code — always use a managed executor.
- Size your thread pool around CPU cores and I/O latency, not request volume.
- Always configure rejection policies to handle overload gracefully instead of crashing.
- Monitor thread count and queue depth as part of your production observability.
run() methods for shared state access.Key takeaways
Common mistakes to avoid
5 patternsCalling run() instead of start()
start() to create a new call stack. Use code reviews and static analysis rules to flag direct run() calls outside tests.Overusing manual thread creation when a simpler approach like an ExecutorService would work
Thread().start() with Executors.newFixedThreadPool(n) and submit tasks. Set bounds on pool size and queue capacity.Not understanding the thread lifecycle (states) leads to deadlocks or zombie threads
Ignoring error handling inside run()
run() go uncaught and kill the thread silently. No error logged, no recovery. The application continues with fewer threads than expected.run() logic in try-catch block that logs the exception. Set a default UncaughtExceptionHandler for the thread pool or thread group to handle unexpected errors.Calling start() twice on the same Thread object
start() call fails because the thread is already in RUNNABLE or TERMINATED state.Interview Questions on This Topic
Explain the difference between start() and run() in the Thread class. Which one creates a new call stack?
run() method in a separate thread. run() simply invokes the run() method in the current thread — it's just a regular method call. Only start() triggers thread creation. Calling run() directly means no new thread is spawned.Frequently Asked Questions
That's Concurrency. Mark it forged?
3 min read · try the examples if you haven't