Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 42

Achieving Thread Safety with `synchronized`

Recommended resources

Welcome back! In our previous lesson, we diagnosed a critical problem in concurrent programming: the race condition. We saw how multiple threads trying to modify shared data can lead to unpredictable and incorrect results. We identified these vulnerable code segments as "critical sections" and established that the solution is to enforce "mutual exclusion"—ensuring only one thread can execute a critical section at a time.

This lesson delivers the cure. We will explore Java's primary tool for enforcing mutual exclusion: the synchronized keyword. You will learn how to apply it to both entire methods and specific blocks of code to make your multithreaded applications thread-safe. Mastering this concept is a fundamental step toward building the robust and reliable backend systems that are central to your goal of enterprise Java development.

1. The Simplest Solution: Synchronized Methods

Let's return to the Counter class from our last lesson, which suffered from a race condition when multiple threads called its increment() method.

// The unsafe class from our previous lesson
public class Counter {
    private int count = 0;

    public void increment() {
        count++; // This is the critical section
    }

    public int getCount() {
        return count;
    }
}

The simplest way to fix this is to add the synchronized keyword to the method signature of the critical section.

// The thread-safe version
public class Counter {
    private int count = 0;

    public synchronized void increment() {
        count++;
    }

    public int getCount() {
        return count;
    }
}

That's it. By adding this single keyword, we have made the increment() method thread-safe. If you were to re-run the coding exercise from the last lesson with this change, you would find that the final count is consistently and correctly 200,000.

The Synchronized Keyword in Java Multithreading - Java Programming

This video provides a great demonstration of this exact scenario. The presenter first shows the race condition in action and then fixes it by adding the synchronized keyword to the method.

Watch the segment from add the synchronized keyword to see the corrected code and the resulting correct output.

But what is actually happening? How does this keyword work?


2. The Intrinsic Lock (Monitor)

Every object in Java has an associated "intrinsic lock," also known as a "monitor." You can think of this lock like a single key to a private room.

When you declare an instance method (a non-static method) as synchronized, you are telling Java the following: before any thread can execute this method, it must first acquire the intrinsic lock of the object instance the method is being called on (i.e., the this object).

This process enforces mutual exclusion:

  1. Acquire Lock: Thread A calls counter.increment(). It attempts to acquire the lock associated with the counter object. Since no other thread holds the lock, Thread A acquires it and enters the method.
  2. Block Others: While Thread A is executing the method, Thread B also calls counter.increment(). Thread B attempts to acquire the same lock (on the same counter object), but finds it is already held by Thread A. Thread B is now blocked and must wait.
  3. Release Lock: Thread A finishes executing increment(), and as it exits the method (either by returning or through an exception), it automatically releases the lock.
  4. Proceed: The JVM can now grant the lock to one of the waiting threads. Thread B acquires the lock and begins executing the method.

This sequence ensures that only one thread can be inside any synchronized instance method on the same object instance at any given time.

This diagram shows the lifecycle of a lock. Thread 1 acquires the lock and executes. Threads 2 and 3 must wait. Once Thread 1 releases the lock, Thread 2 can acquire it and continue the process.

It's crucial to understand that the lock is tied to the object instance, not the method itself.

Java Synchronized - The synchronized keyword in Java and Java synchronized blocks and methods

Jakob Jenkov provides an excellent conceptual explanation of this mechanism, clarifying that synchronized methods are effectively synchronized on the this monitor object.

Watch from synchronized means. Pay close attention to the diagrams that distinguish between threads accessing one shared instance versus threads accessing two separate instances. This will solidify your understanding of what the lock is attached to.


3. Finer-Grained Locking: Synchronized Blocks

Synchronizing an entire method is simple, but it can be inefficient. A method might contain several lines of code, but only a few of them might make up the actual critical section. Locking the entire method means that threads are blocked from executing even the non-critical parts of the code. This can reduce the potential for concurrency and harm performance—a key concern in backend systems.

For more precise control, Java allows you to use synchronized blocks.

This image compares synchronized methods and blocks, highlighting that blocks offer more fine-grained control and are preferred for performance by minimizing the scope of the lock.

A synchronized block locks on a specific object only for the duration of that block, allowing you to protect just the critical lines of code.

public void someMethod() {
    // 1. Non-critical code that can be run by multiple threads concurrently.
    System.out.println("Preparing to update...");

    // 2. Start of the synchronized block.
    // Threads will contend for the lock on 'this' object here.
    synchronized (this) {
        // 3. This is the critical section. Only one thread can be here at a time.
        // count++;
    } // The lock is released here.

    // 4. More non-critical code.
    System.out.println("Update complete.");
}

Notice the synchronized(this) syntax. This creates a synchronized block that uses the current object's intrinsic lock, making it functionally equivalent to declaring the entire method synchronized if the block covers the whole method body. However, its power lies in allowing you to leave non-critical code outside the block, improving performance.

Guide to the Synchronized Keyword in Java

The Baeldung guide provides a clear explanation and code example for synchronized blocks.

Please read the section titled "3.3. Synchronized Blocks Within Methods". Focus on the syntax and the explanation of the monitor object. Notice how you can pass this to the block to synchronize on the current instance.


4. Choosing Your Lock Object

While synchronized(this) is common, you are not limited to locking on the current instance. You can use any object as a monitor. A common and recommended practice is to create a dedicated, private object to serve as a lock.

public class FineGrainedCounter {
    private long count = 0;
    
    // A private, final object used only for locking.
    private final Object lock = new Object();

    public void someMethod() {
        // ... non-critical code ...

        synchronized (lock) {
            count++; // Critical section
        }

        // ... more non-critical code ...
    }
}

Using a private lock object has a few advantages:

  • Encapsulation: It prevents outside code from acquiring your object's lock and causing unexpected contention or deadlocks. Your locking mechanism is a private implementation detail.
  • Granularity: You can use different lock objects to protect different, independent shared resources within the same class. This allows one thread to access resource A while another thread accesses resource B, maximizing concurrency.

Java Synchronized - The synchronized keyword in Java and Java synchronized blocks and methods

Let's watch another segment from Jakob Jenkov's video. He explains this concept of using different monitor objects for different synchronized blocks.

Watch from different monitor objects. This part clearly illustrates how synchronizing on two different lock objects allows two threads to execute code concurrently within the same instance.


5. Static Synchronization

What if the shared resource is a static field? An instance lock (this or a private instance field) won't work, because static members belong to the class, not to any particular object instance.

For this scenario, Java provides static synchronization.

  • A static synchronized method acquires the lock on the class's Class object (e.g., Counter.class).
  • A synchronized block can also lock on the Class object: synchronized (Counter.class) { ... }.

Since there is only one Class object per class per classloader, this lock is effectively global for all instances of that class. Only one thread in the entire JVM can be inside a static synchronized block or method for that class at any given time.

Guide to the Synchronized Keyword in Java

Let's revisit the Baeldung article to see how static synchronization is implemented.

Read the section "3.2. Synchronized Static Methods". The text explains that these methods are synchronized on the Class object. Then, look again at the end of section 3.3, where it shows the syntax for a static synchronized block: the static example.


6. Coding Exercise: Applying Synchronization

Now it's time to put this into practice. Take the code you wrote for the previous lesson's exercise, which produced a race condition.

  1. Fix with a Synchronized Method: Add the synchronized keyword to your increment() method. Run the program multiple times. Verify that the final count is now consistently 200,000.
  2. Fix with a Synchronized Block: Remove the synchronized keyword from the method signature. Instead, wrap the count++ line inside a synchronized(this) block. Run it again and verify the result is still correct.
  3. (Optional) Fix with a Private Lock: Add a private final Object lock = new Object(); field to your Counter class. Change your synchronized block to use synchronized(lock). Confirm that this also correctly solves the race condition.

This exercise will give you hands-on experience with the different ways to apply synchronization and reinforce that the key is locking on a shared monitor object.


Conclusion

In this lesson, we moved from identifying the problem of race conditions to implementing the solution. You now have the primary Java tool for ensuring that critical sections of your code are thread-safe.

Key Takeaways:

  • The synchronized keyword is used to enforce mutual exclusion, preventing race conditions.
  • It works by having threads acquire an intrinsic lock (or monitor) associated with a Java object.
  • Synchronized instance methods are a simple way to protect code. They lock on the current object instance (this).
  • Synchronized blocks (synchronized(object) { ... }) provide finer-grained control, which is often better for performance. You can choose which object to lock on.
  • Using a private final Object as a dedicated lock is a good practice for encapsulation and control.
  • Static synchronized methods/blocks lock on the Class object, protecting static data shared across all instances.

So far, we've focused on creating threads manually and managing shared data between them. In our next lesson, we will explore the ExecutorService framework, a powerful and flexible API for managing thread execution and lifecycles, which is the standard approach in modern enterprise applications.