Java
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Object-Oriented Java
-
Section 4: Collections Framework
-
Section 5: Exception Handling
-
Section 6: Generics
-
Section 7: Functional Java
-
Section 8: Concurrency
-
Section 9: I/O and NIO
-
Section 10: JVM Internals
-
Section 11: Modern Java Features
-
Section 12: Build Tools and Project Structure
-
Section 13: Testing
-
Section 14: Databases and Persistence
-
Section 15: Networking
-
Section 16: Design and Best Practices
-
Section 17: Reflection and Annotations
-
Section 18: Logging and Diagnostics
-
Section 19: Date, Time, and Internationalization
-
Section 20: Java Platform Module System
-
Section 21: Security in Java
-
Section 22: Advanced Collections and Data Structures
-
Section 23: More Concurrency Patterns
-
Section 24: Compression, Files, and System Integration
-
Section 25: GUI Programming
-
Section 26: Practical Projects
-
Section 27: Data Structures and Algorithms
-
Section 28: Interview and Algorithm Practice
-
Section 29: JSON and Data Interchange
-
Section 30: More Concurrency Utilities
-
Section 31: More Collections and Streams Practice
-
Section 32: More File and System Programming
-
Section 33: Standard Library Deep Dive
-
Section 34: More Practice and Drills
-
Section 35: More Testing and Quality
-
Section 36: More Design Patterns and Architecture
-
Section 37: Career and Ecosystem
-
Section 38: More OOP and Architecture Practice
-
Section 39: More Enterprise Concepts
-
Section 40: Advanced JavaFX
-
Section 41: More Interview Practice
204: Common Concurrency Pitfalls and Deadlocks
I've spent a lot of time reviewing code from developers who are just starting to get comfortable with multi-threading, and there is one misconception that comes up almost every single time: the belief that "if I just synchronize every method that touches shared data, the code will be thread-safe."
It sounds logical, right? If you wrap everything in synchronized blocks, you're essentially putting a "one person at a time" sign on your data. But in reality, this approach doesn't just kill your performance—it's the fastest way to introduce a deadlock that will freeze your entire production environment.
The "More Locks Equals More Safety" Myth
Let's look at a classic example: a simple bank transfer. You have two accounts, and you want to move money from one to the other. To be "safe," you might decide to lock both account objects to ensure no other thread interferes with the balances.
public void transfer(Account from, Account to, double amount) {
synchronized (from) {
synchronized (to) {
if (from.getBalance() >= amount) {
from.withdraw(amount);
to.deposit(amount);
}
}
}
}
At first glance, this looks bulletproof. But here is where it falls apart. Imagine Thread A is transferring money from Account 1 to Account 2, and at the exact same millisecond, Thread B is transferring money from Account 2 to Account 1. Thread A grabs the lock for Account 1. Thread B grabs the lock for Account 2. Now, Thread A is waiting for Account 2, and Thread B is waiting for Account 1. Neither will ever let go. Your application has just hit a deadlock.
The Deadly Embrace: How Deadlocks Actually Happen
A deadlock isn't just "bad luck"; it's a structural failure. It happens when you have a circular dependency of locks. In the example above, the order of acquisition was dependent on the method arguments. Because the arguments were swapped between the two threads, the locking order was swapped, creating a cycle.
I usually tell my juniors to think of locks like a set of keys. If two people both need the Kitchen key and the Bedroom key to finish their chores, but one person grabs the Kitchen key and the other grabs the Bedroom key, they'll spend the rest of the afternoon staring at each other in the hallway.
When Volatile Isn't Enough
Another pitfall I see frequently is the confusion between visibility and atomicity. You'll often see someone use the volatile keyword and assume they've solved their concurrency problem. volatile is great for telling the JVM, "Don't cache this variable locally; always read it from main memory." It solves the visibility problem.
But it does nothing for atomicity. If you have a volatile int counter = 0; and you call counter++ from ten different threads, you will almost certainly end up with a final count less than 10. Why? Because counter++ is actually three separate operations: read, increment, and write. A thread can be preempted right in the middle of those steps. If you need atomicity, you need synchronized or, better yet, the java.util.concurrent.atomic package.
Breaking the Cycle with Consistent Lock Ordering
So, how do we actually fix that bank transfer deadlock? The secret is to ensure that locks are always acquired in a global, consistent order, regardless of who is sending the money.
One simple trick is to use a unique identifier (like an account ID) to determine who gets locked first. I've found this to be the most reliable way to prevent circular dependencies without over-complicating the logic.
public void transfer(Account from, Account to, double amount) {
Account first = from.getId() < to.getId() ? from : to;
Account second = from.getId() < to.getId() ? to : from;
synchronized (first) {
synchronized (second) {
if (from.getBalance() >= amount) {
from.withdraw(amount);
to.deposit(amount);
}
}
}
}
By sorting the locks based on the ID, both Thread A and Thread B will attempt to lock the accounts in the exact same order. If Thread A gets the lock for Account 1, Thread B will wait at the first synchronized block instead of grabbing Account 2 and causing a stalemate. It's a small change, but it's the difference between a stable system and one that crashes randomly under load.
📋 Practical Task
Exercise: Resolving the Warehouse Inventory Deadlock
You are maintaining a warehouse management system. The system has a Product class and an InventoryManager. Currently, the swapQuantities method is causing intermittent deadlocks when two different threads try to swap stock levels between the same two products in opposite directions.
Your Task: Modify the InventoryManager.java code below to eliminate the deadlock. You must ensure that no matter which product is passed as p1 or p2, the locks are always acquired in a consistent order. Assume the Product class has a public int getProductId() method.
class Product {
private final int id;
private int quantity;
public Product(int id, int quantity) {
this.id = id;
this.quantity = quantity;
}
public int getProductId() { return id; }
public int getQuantity() { return quantity; }
public void setQuantity(int q) { this.quantity = q; }
}
class InventoryManager {
// FIX THIS METHOD to prevent deadlocks
public void swapQuantities(Product p1, Product p2) {
synchronized (p1) {
synchronized (p2) {
int temp = p1.getQuantity();
p1.setQuantity(p2.getQuantity());
p2.setQuantity(temp);
System.out.println("Swapped " + p1.getProductId() + " and " + p2.getProductId());
}
}
}
}
There are no comments for now.