๐Ÿงต Thread Best Practices in Java

VijayVijay
4 min read

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, or

  • Implementing 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 blocks

  • Use 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 PracticeWhy It Matters (Principle)
Use Runnable over ThreadDecouples task from thread, allows better reuse and composition
Keep synchronized minimalReduces contention, improves parallelism, prevents serialization
Handle interruption properlyEnables graceful exit, prevents hanging threads and resource leaks
Avoid thread starvationEnsures fair CPU usage, prevents deadlocks or unresponsive services
Use concurrency utilitiesSimplifies coding, avoids subtle bugs, leverages optimized thread-safe implementations
Always shutdown ExecutorServicePrevents resource leaks and JVM hang
Prefer immutable shared dataRemoves need for locks, ensures thread-safety without synchronization
0
Subscribe to my newsletter

Read articles from Vijay directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Vijay
Vijay