|
Voiced by Amazon Polly |
Overview
Threads make Java backend systems fast. They also make bugs nearly impossible to reproduce. This article walks through the core concepts, common failure modes, and practical patterns for writing multithreaded code that holds up.
Pioneers in Cloud Consulting & Migration Services
- Reduced infrastructural costs
- Accelerated application deployment
Multithreading
A Java process can run multiple threads simultaneously, each executing independently while sharing the same memory. For backend systems, this matters because a thread blocked on a slow DB call shouldn’t stall everything else, and independent tasks like fetching user data and order history shouldn’t run sequentially when they don’t have to.
Creating Threads
Three options exist, but there’s a clear winner for production. Extending Thread works but limits inheritance. Runnable is cleaner. ExecutorService is what you actually want, it manages a pool of reusable threads instead of spinning up new ones per request.
|
1 2 3 |
ExecutorService executor = Executors.newFixedThreadPool(3); executor.submit(() -> System.out.println(Thread.currentThread().getName())); executor.shutdown(); |

Thread Lifecycle
|
1 |
NEW → RUNNABLE → RUNNING → BLOCKED/WAITING → TERMINATED |
The BLOCKED state is where things quietly break. A thread waiting on a lock held indefinitely sits there, no error, no timeout. This is why deadlocks are hard to spot.
Thread Pools
Creating a new thread per request seems fine at low traffic. At scale, it tanks performance, threads allocate stack memory, and context switching between hundreds of them burns CPU without doing real work.
|
1 2 3 |
ExecutorService executor = Executors.newFixedThreadPool(5); executor.submit(() -> processRequest()); executor.shutdown(); |
- newFixedThreadPool(n) — predictable concurrency, API, or job processing
- newCachedThreadPool() — short-lived burst tasks
- newSingleThreadExecutor() — order-sensitive work like audit logging
Synchronization
count++ looks atomic, but it isn’t, it’s three steps: read, increment, write. Two threads hitting it at the same time can both read the same value and write back the same result. That’s a race condition.
|
1 2 3 4 |
class Counter { int count = 0; public synchronized void increment() { count++; } } |
Synchronized lets only one thread in at a time. The tradeoff is throughput, lock the smallest section of code you need to protect, not the whole method.
What Goes Wrong?
Race conditions are subtle, tests pass, production breaks under load. Fix with synchronized, ReentrantLock, or AtomicInteger.
Deadlocks freeze the app with no error or log output:
|
1 2 |
Thread 1 holds Lock A, waiting for Lock B Thread 2 holds Lock B, waiting for Lock A |
Always acquire locks in the same order. Use tryLock with a timeout as a safety net.
Thread starvation occurs when lower-priority threads are never scheduled, often due to a misconfigured thread pool.
Too many threads, context switching overhead eventually hurts more than the concurrency helps. Match thread count to workload: close to core count for compute, higher for I/O-bound work.
Real-World Backend Use Cases
- Parallel API calls – Fetch data from three services concurrently with Future. Total latency becomes the slowest call, not the sum of all three.
- Async processing — Fire off emails or audit writes in a background thread. The response goes out immediately.
- Batch jobs — Migrations, reports, and file imports run in dedicated threads without competing with live traffic.
Best Practices
- Use ExecutorService, not new Thread().
- Minimize shared mutable state — immutable objects avoid a whole class of bugs.
- Synchronize only the critical section, not the entire method.
- Always call the executor.shutdown() — forgetting it leaks threads in long-running services.
- Use ConcurrentHashMap and CopyOnWriteArrayList in place of their non-thread-safe counterparts.
- Keep heavy tasks off the main thread — emails, reports, external calls should always be async.
- Learn thread dumps — JConsole and VisualVM are how concurrency bugs get diagnosed in production.
Conclusion
The primitives are simple, threads, locks, and pools. The hard part is knowing when you need them and recognizing the failure modes before they hit production. Get ExecutorService, synchronization, and concurrent collections right, and you’ll cover the majority of real-world backend concurrency scenarios. The rest comes with experience.
Drop a query if you have any questions regarding Multithreading, and we will get back to you quickly.
Making IT Networks Enterprise-ready – Cloud Management Services
- Accelerated cloud migration
- End-to-end view of the cloud environment
About CloudThat
FAQs
1. What is the difference between Process vs Thread?
ANS: – A process has its own memory. Threads share memory within a process, which is exactly why uncoordinated access to the same variable causes problems.
2. Why not just use new Thread() everywhere?
ANS: – It’s expensive and uncontrolled. ExecutorService gives you a fixed pool, thread reuse, and task queuing.
3. What causes a race condition?
ANS: – Non-atomic operations on shared data without synchronization, two threads reading and writing the same variable will step on each other.
WRITTEN BY Shashank Shekhar
Login

June 22, 2026
PREV
Comments