Java Synchronization: synchronized Keyword, Locks, Critical Sections and Thread Safety

0

Synchronization in Java

Multithreading becomes genuinely interesting when multiple threads start working with the same data. Sharing data allows threads to cooperate efficiently, but it also creates a serious problem: two threads may access or modify the same resource at the same time.

For example, imagine two bank employees processing withdrawals from the same account simultaneously. If both employees read the old balance before either withdrawal is applied, the final balance can become incorrect. The problem is not that either employee performed the calculation incorrectly; the problem is that the operations were allowed to interfere with each other.

Java provides synchronization to control concurrent access to shared mutable state and prevent unsafe interleaving of critical operations.

Core idea: Synchronization coordinates threads so that operations involving shared state happen according to defined locking and visibility rules instead of allowing arbitrary concurrent access.

Why Do We Need Synchronization?

Consider a simple counter shared by multiple threads.

class Counter {

    int count = 0;

    void increment() {
        count++;
    }
}

At first glance, count++ looks like one operation. It is not. Conceptually, it involves reading the current value, calculating a new value, and writing the result back.

count++;
    
// Conceptually similar to:
//
// 1. Read count
// 2. Add 1
// 3. Write the new value

If two threads perform these steps concurrently, their operations can interleave in an unsafe way.

Real-World Analogy

Imagine a single whiteboard showing the number of available seats in a classroom. Two employees independently read "1 seat available" and both decide to assign that seat to different students.

The problem is not reading the number. The problem is allowing the read → decision → update sequence to overlap without coordination.

Synchronization is like placing a temporary key on the whiteboard. Only the employee holding the key can perform the protected operation. Once finished, the key is released for another employee.

What Is a Critical Section?

A critical section is a section of code that accesses shared mutable state and therefore must be executed under appropriate coordination.

class Counter {

    private int count = 0;

    synchronized void increment() {

        count++;
    }
}

The increment operation is the critical section because multiple threads can potentially modify the same count variable.

The synchronized Keyword

Java provides the synchronized keyword for intrinsic locking. It can be applied to methods and blocks.

When a thread enters a synchronized region protected by the same monitor, another competing thread must wait until that monitor becomes available.

class Counter {

    private int count = 0;

    public synchronized void increment() {

        count++;
    }

    public int getCount() {

        return count;
    }
}

Here, only one thread at a time can execute the synchronized increment() method for a given Counter object.

Remember: synchronized does not mean "make the entire application single-threaded." It protects specific operations or regions associated with a particular monitor.

Synchronized Instance Method

When an instance method is declared synchronized, the thread must acquire the monitor associated with the current object before entering the method.

class Printer {

    public synchronized void printMessage() {

        System.out.println(
            "Printing document"
        );
    }
}

For an instance method, the effective lock is associated with this, meaning the current object.

Printer printer = new Printer();

// These calls use the same object monitor.
printer.printMessage();

Synchronized Block

Sometimes synchronizing an entire method is unnecessarily broad. A synchronized block allows you to protect only the section that actually requires mutual exclusion.

class Counter {

    private int count = 0;

    public void increment() {

        synchronized (this) {

            count++;
        }
    }
}

This can make the synchronization boundary more precise and can allow unrelated work in the method to execute without holding the lock.

Why Prefer a Narrow Critical Section?

Locks should generally be held for the shortest practical duration. Holding a lock while performing slow or unrelated work can force other threads to wait unnecessarily.

public void process() {

    // Expensive work that does not
    // require the shared lock.
    performCalculation();

    synchronized (this) {

        updateSharedState();
    }
}

The goal is not to minimize synchronization at all costs. The goal is to protect exactly the shared state that requires coordination while avoiding unnecessary lock contention.

Using a Dedicated Lock Object

A synchronized block does not have to lock on this. A private object can be used as a dedicated monitor.

class Account {

    private final Object lock = new Object();

    private int balance = 1000;

    public void withdraw(int amount) {

        synchronized (lock) {

            if (balance >= amount) {

                balance -= amount;
            }
        }
    }
}

Using a private lock can prevent unrelated code from intentionally synchronizing on the same public object.

Design insight: A private lock gives the class stronger control over who can participate in its synchronization protocol.

Static synchronized Method

A static synchronized method uses the monitor associated with the class object rather than an individual instance.

class Utility {

    public static synchronized void process() {

        System.out.println(
            "Processing shared class-level state"
        );
    }
}

Conceptually, the lock is associated with Utility.class.

public static void process() {

    synchronized (Utility.class) {

        System.out.println(
            "Processing"
        );
    }
}

Therefore, an instance synchronized method and a static synchronized method do not use the same monitor.

Instance Lock vs Class Lock

Type Monitor Used
synchronized instance method The current object instance.
synchronized(this) The current object instance.
synchronized(lock) The specified lock object.
synchronized static method The Class object.
synchronized(SomeClass.class) The specified Class object.

Synchronization Provides Mutual Exclusion

One major purpose of synchronization is mutual exclusion. When multiple threads attempt to enter synchronized regions guarded by the same monitor, only one can own that monitor at a time.

class SharedResource {

    public synchronized void use() {

        System.out.println(
            "Thread entered critical section"
        );

        try {

            Thread.sleep(1000);

        }
        catch (InterruptedException e) {

            Thread.currentThread().interrupt();
        }
    }
}

If two threads call use() on the same SharedResource object, one thread must acquire the object's monitor before entering. The other waits until the monitor is released.

Synchronization Also Provides Memory Visibility

Synchronization is not only about preventing two threads from entering the same critical section simultaneously. Correct synchronization also establishes important memory visibility guarantees between threads.

Without proper synchronization or another safe publication mechanism, one thread's updates to shared data may not become visible to another thread in the way the programmer expects.

This is why synchronization is both an atomicity tool and a visibility mechanism.

Important: Multithreading correctness is not just about preventing simultaneous execution. Threads also need a well-defined way to observe each other's changes.

Example: Safe Counter

class Counter {

    private int count = 0;

    public synchronized void increment() {

        count++;
    }

    public synchronized int getCount() {

        return count;
    }
}

public class SynchronizationDemo {

    public static void main(String[] args)
            throws InterruptedException {

        Counter counter = new Counter();

        Thread first = new Thread(() -> {

            for (int i = 0; i < 1000; i++) {
                counter.increment();
            }
        });

        Thread second = new Thread(() -> {

            for (int i = 0; i < 1000; i++) {
                counter.increment();
            }
        });

        first.start();
        second.start();

        first.join();
        second.join();

        System.out.println(
            "Final count: "
            + counter.getCount()
        );
    }
}

Both threads modify the same Counter object. Because increment() is synchronized, the increment operation is protected by the object's monitor.

After both threads complete, the expected final count is 2000.

Synchronization Does Not Automatically Make Everything Safe

A common misconception is that declaring one method synchronized makes the entire object thread-safe.

class Counter {

    private int count = 0;

    public synchronized void increment() {

        count++;
    }

    public int getCount() {

        return count;
    }
}

The increment operation is protected, but the design of the getter and the overall class still needs to be considered carefully. Thread safety is a property of the complete interaction between shared state and all ways that state can be accessed or modified.

Synchronization must therefore be designed around the invariants of the object, not applied randomly to individual methods.

Synchronization and Different Objects

An instance synchronized method locks the particular object on which it is invoked. Two different objects have different monitors.

Counter first = new Counter();
Counter second = new Counter();

Thread threadOne = new Thread(() -> {
    first.increment();
});

Thread threadTwo = new Thread(() -> {
    second.increment();
});

threadOne.start();
threadTwo.start();

The two calls use different Counter objects and therefore different instance monitors. Synchronization on one object does not automatically block synchronized methods on another object.

Common Beginner Mistakes

  • Thinking synchronized automatically makes every field in a class thread-safe.
  • Synchronizing on different objects when threads actually need to coordinate through the same lock.
  • Holding a lock while performing slow, unrelated operations.
  • Using a public object as a lock without considering external code that could also synchronize on it.
  • Assuming synchronization is needed only for writing and not for visibility of shared state.
  • Adding synchronized everywhere without understanding contention and the class's actual concurrency requirements.

Best Practices

  • Identify shared mutable state before deciding where synchronization is needed.
  • Keep critical sections as small as practical.
  • Use a private lock object when a dedicated synchronization boundary improves encapsulation.
  • Protect related state changes under the same synchronization strategy.
  • Avoid holding locks during slow I/O or unrelated expensive operations whenever possible.
  • Prefer higher-level concurrency utilities when they provide a clearer and safer design than manual synchronization.

Interview Insights

Question: What is synchronization in Java?

Synchronization is a mechanism for coordinating access to shared resources so that multiple threads can safely interact with shared mutable state according to defined locking and visibility rules.

Question: What does synchronized do?

It establishes a monitor-based synchronization boundary. For competing synchronized regions using the same monitor, only one thread can hold that monitor at a time, and synchronization also provides memory visibility guarantees.

Question: What object does a synchronized instance method lock?

It locks the monitor associated with the current object, effectively this.

Question: What does a static synchronized method lock?

It locks the monitor associated with the class object.

Question: Why use synchronized blocks instead of synchronized methods?

A synchronized block can protect only the code that actually requires synchronization, which can reduce unnecessary lock contention and make the synchronization boundary more precise.

Quick Revision

Concept Key Point
Synchronization Coordinates access to shared mutable state.
Critical Section Code that requires protected access to shared state.
synchronized instance method Uses the current object's monitor.
synchronized static method Uses the Class object's monitor.
synchronized block Protects a specific section using a chosen monitor.
Mutual Exclusion Only one competing thread can own the same monitor at a time.
Visibility Synchronization establishes important memory visibility guarantees.
Private Lock Provides an encapsulated synchronization boundary controlled by the class.

Synchronization is fundamentally about controlling access to shared state so that threads do not interfere with one another and can correctly observe each other's updates. The most important lesson is not simply "add synchronized"; it is to identify the shared state, define the critical section, choose the correct monitor, and protect the object's invariants consistently. Once this foundation is clear, the next chapter—Race Conditions—will reveal what can go wrong when shared operations are allowed to interleave without proper coordination.

Post a Comment

0Comments
Post a Comment (0)