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
81: Synchronization and Locks
I've spent a lot of time reviewing PRs for junior and mid-level devs, and there is one mistake that pops up constantly when it comes to concurrency. It usually looks something like this: the developer has a shared resource, they realize they have a race condition, and their solution is to simply slap the synchronized keyword on every method that touches that resource. They assume that because the methods are "synchronized," the data inside them is now magically protected from all concurrent access.
The Myth: Synchronized Methods Protect the Data
Here is the problem: synchronized does not protect data. It protects a lock (also called a monitor). When you mark an instance method as synchronized, you are telling Java: "Before any thread enters this method, it must acquire the lock associated with this specific object instance."
Consider this snippet. You might think the balance is safe because both methods are synchronized:
public class BankAccount {
private int balance = 100;
public synchronized void deposit(int amount) {
balance += amount;
}
public synchronized void withdraw(int amount) {
balance -= amount;
}
}
If you have one BankAccount object and ten threads hitting it, this works fine. But what happens if your architecture creates two different BankAccount objects that both point to the same shared database record or a static shared variable? Since each object has its own lock, Thread A can be in deposit() on Object 1, and Thread B can be in withdraw() on Object 2 at the exact same time. They aren't competing for the same lock, so they both enter their respective methods, and your data gets corrupted. I've seen this crash production systems because the developer thought the method was the gatekeeper, rather than the object instance.
Locking on Private Objects for Better Control
To avoid the "wrong lock" trap, I rarely use synchronized methods anymore. Instead, I prefer using a private lock object. This ensures that no one outside the class can accidentally (or maliciously) acquire your lock and cause a deadlock by synchronizing on your instance from the outside.
public class BankAccount {
private int balance = 100;
private final Object lock = new Object(); // Our dedicated gatekeeper
public void deposit(int amount) {
synchronized(lock) {
balance += amount;
}
}
public void withdraw(int amount) {
synchronized(lock) {
balance -= amount;
}
}
}
Now, it doesn't matter how the methods are called; they are all fighting for the same lock object. It's a much cleaner pattern because it decouples the locking logic from the object's public identity.
When synchronized Isn't Enough: ReentrantLock
Sometimes, the synchronized block is too blunt a tool. It's "all or nothing"—a thread either gets the lock or it blocks indefinitely until it does. In a high-performance system, blocking a thread forever is a recipe for a bottleneck. This is where java.util.concurrent.locks.ReentrantLock comes in.
The ReentrantLock gives you superpowers that synchronized doesn't. My favorite is tryLock(). Instead of waiting forever, a thread can check if the lock is available. If it is, it takes it; if not, it can do something else (like logging a timeout or trying a different resource) instead of just freezing.
private final ReentrantLock lock = new ReentrantLock();
public void updateInventory() {
if (lock.tryLock()) {
try {
// Perform the update
} finally {
lock.unlock(); // Always unlock in a finally block!
}
} else {
System.out.println("System busy, please try again later.");
}
}
One word of warning: unlike synchronized, Java won't automatically release a ReentrantLock when the method ends. If you forget that finally { lock.unlock(); } block, you've just created a permanent deadlock. I've spent many late nights hunting down bugs that were simply missing unlock() calls.
📋 Practical Task
Fixing the Race Condition in a Multi-Threaded Warehouse Manager
You have been handed a WarehouseManager class that is supposed to track the number of items in stock across multiple threads. However, the current implementation uses synchronized methods on an instance level, but the warehouse is using a static` shared stock count, leading to data corruption when multiple manager instances are created.
Your Goal: Refactor the provided code to ensure that the stockCount is protected regardless of how many WarehouseManager instances are instantiated. You should implement a private final lock object to ensure thread safety.
public class WarehouseManager {
private static int stockCount = 1000;
// This is currently failing because it locks on 'this'
// but the resource is static!
public synchronized void shipItem() {
if (stockCount > 0) {
stockCount--;
}
}
public synchronized void receiveItem() {
stockCount++;
}
public static int getStockCount() {
return stockCount;
}
}
Requirements:
- Create a
private static final Objectto act as the lock. - Replace the
synchronizedmethod modifiers withsynchronized(lock)blocks inside the methods. - Ensure that
shipItemandreceiveItemare fully thread-safe across all instances of the class.
There are no comments for now.