Skip to Content
Course content

81: Synchronization and Locks

Click on the "Edit" button in the top corner of the screen to edit your slide content.

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 Object to act as the lock.
  • Replace the synchronized method modifiers with synchronized(lock) blocks inside the methods.
  • Ensure that shipItem and receiveItem are fully thread-safe across all instances of the class.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.