๐งต Thread Best Practices in Java

Table of contents
Threads bring power, but with power comes responsibility. Misused threads lead to race conditions, starvation, performance bottlenecks, or worse โ broken applications.
Hereโs a comprehensive guide to writing clean, reliable, and efficient multi-threaded Java code, explained from first principles.
1. Use Runnable
over extending Thread
๐ Core Principle
In Java, you can create threads by either:
Extending the
Thread
class, orImplementing the
Runnable
interface.
But why prefer Runnable
?
Because extending a class tightly couples your logic to threading behavior, and Java only supports single inheritance. That limits reuse.
First Principles Reasoning
If Thread
is a worker, then your task logic shouldn't be embedded inside the worker itself. Instead, tasks should be defined separately and passed to a thread. This keeps code modular.
โ
Good Example: Using Runnable
๐ซ Not Recommended: Extending Thread
โ Why Runnable is Better
Encourages composition over inheritance
Enables reuse (you can pass the same
Runnable
to multiple threads)Doesnโt block you from extending another useful class
2. Minimize Synchronization to Avoid Performance Bottlenecks
๐ Core Principle
Java provides tools like synchronized
blocks to prevent race conditions. But overusing synchronization serializes your program โ turning a multithreaded system into a single-threaded one.
First Principles Reasoning
When you mark a method or block as synchronized
, only one thread can execute it at a time for a given lock. This is like having multiple people share one bathroom. If one thread holds the lock, others must wait โ even if they don't interfere with each other.
Bad Example: Over-synchronizing
Even the getCount()
method is synchronized, though it doesnโt modify anything. Thatโs unnecessary and hurts performance.
โ
Better: Use synchronized
only where needed
๐ก Tip
Avoid holding locks during I/O, network, or long computations.
Use finer-grained locking or atomic variables when possible.
3. Handle Interruption Gracefully
๐ Core Principle
When a thread is no longer needed (e.g., app shutdown), itโs asked to stop via interrupt()
. But the thread must voluntarily check and exit.
First Principles Reasoning
Java doesnโt forcibly kill threads. Thatโs dangerous. Instead, it sets an internal flag (interrupted = true
) and leaves it up to the thread to react.
If your thread ignores the interruption, it may keep running forever โ wasting CPU or preventing shutdown.
โ Good Example: Checking interruption
What Happens If Not Handled?
Threads run forever
Application shutdown gets stuck
Resource leaks or inconsistent state
4. Avoid Thread Starvation
๐ Core Principle
Thread starvation happens when some threads get all the CPU time and others get very little or none.
First Principles Reasoning
Imagine a restaurant with 10 hungry guests but only 2 waiters. If one waiter keeps serving the same 2 people, others starve.
In threads, this happens due to:
Priority misconfiguration
Poor locking (e.g., holding a lock too long)
Not using fair queues (like in thread pools)
Example of starvation
โ Best Practice
Keep critical sections short
Avoid infinite loops inside
synchronized
blocksUse fair thread pools (
Executors.newFixedThreadPool()
with bounded queue)
5. Use Java Concurrency Utilities
๐ Core Principle
The java.util.concurrent
package provides battle-tested, thread-safe tools like:
ExecutorService
ConcurrentHashMap
Semaphore
,ReentrantLock
,CountDownLatch
First Principles Reasoning
Why reinvent the wheel?
Concurrency is hard. Subtle mistakes can lead to deadlocks, race conditions, or poor performance. Instead of building thread management from scratch, use tools designed and tested for those use cases.
โ
Example: Using ExecutorService
Benefits
Automatic thread reuse
Queueing tasks
Graceful shutdown
Managing large-scale concurrency with ease
6. Always Shutdown Executors
If you use thread pools, you must shut them down to prevent your app from hanging.
Failing to shut down means your app wonโt exit because thread pool threads are still alive.
7. Prefer Immutable Data for Shared Use
Immutable objects can be safely shared between threads without locking.
โ Example
Immutable means: if data never changes, there's no race to access it.
๐ Summary: Best Practices Table
Best Practice | Why It Matters (Principle) |
Use Runnable over Thread | Decouples task from thread, allows better reuse and composition |
Keep synchronized minimal | Reduces contention, improves parallelism, prevents serialization |
Handle interruption properly | Enables graceful exit, prevents hanging threads and resource leaks |
Avoid thread starvation | Ensures fair CPU usage, prevents deadlocks or unresponsive services |
Use concurrency utilities | Simplifies coding, avoids subtle bugs, leverages optimized thread-safe implementations |
Always shutdown ExecutorService | Prevents resource leaks and JVM hang |
Prefer immutable shared data | Removes need for locks, ensures thread-safety without synchronization |
Subscribe to my newsletter
Read articles from Vijay directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
