Skip to content

Synchronized Methods

We know that Java programs rely on synchronized to synchronize threads. When using synchronized, it is crucial to determine which object to lock.

Allowing threads to choose their own lock objects can lead to confusing code logic and hinder encapsulation. A better approach is to encapsulate the synchronized logic within the methods themselves. For example, consider the following Counter class:

java
public class Counter {
    private int count = 0;

    public void add(int n) {
        synchronized(this) {
            count += n;
        }
    }

    public void dec(int n) {
        synchronized(this) {
            count -= n;
        }
    }

    public int get() {
        return count;
    }
}

By doing this, threads calling the add() and dec() methods do not need to worry about the synchronization logic, as the synchronized blocks are handled internally within these methods. Additionally, notice that the synchronized keyword locks on this, meaning the current instance. This allows multiple Counter instances to operate independently and concurrently without interference:

java
var c1 = new Counter();
var c2 = new Counter();

// Threads operating on c1:
new Thread(() -> {
    c1.add(1);
}).start();
new Thread(() -> {
    c1.dec(1);
}).start();

// Threads operating on c2:
new Thread(() -> {
    c2.add(1);
}).start();
new Thread(() -> {
    c2.dec(1);
}).start();

Now, the Counter class is thread-safe, meaning it can be safely used in a multithreaded environment. Java's standard library class java.lang.StringBuffer is also thread-safe.

There are other immutable classes, such as String, Integer, and LocalDate, where all member variables are final. In a multithreaded context, these classes are thread-safe because threads can only read but not modify them. Similarly, classes like Math, which only provide static methods without member variables, are also thread-safe.

Note: Unless explicitly stated otherwise, a class is not thread-safe by default.

Analyzing the Counter Class

Let’s examine the Counter class:

java
public class Counter {
    public void add(int n) {
        synchronized(this) { // Locking on this
            count += n;
        } // Unlocking
    }
    // ...
}

When we lock on the this instance, it is equivalent to declaring the method as synchronized. The following two method implementations are equivalent:

Implementation 1: Using synchronized Block

java
public void add(int n) {
    synchronized(this) { // Lock on this
        count += n;
    } // Unlock
}

Implementation 2: Using synchronized Method

java
public synchronized void add(int n) { // Lock on this
    count += n;
} // Unlock

Therefore, a method marked with synchronized ensures that the entire method is treated as a synchronized block, locking on the current instance (this).

Synchronizing Static Methods

Consider what happens if we add the synchronized keyword to a static method:

java
public synchronized static void test(int n) {
    // ...
}

For static methods, there is no this instance because static methods belong to the class, not to any particular instance. However, every class has a Class instance automatically created by the JVM. Therefore, synchronizing a static method locks on the class's Class instance. The above synchronized static method is effectively equivalent to:

java
public class Counter {
    public static void test(int n) {
        synchronized(Counter.class) {
            // ...
        }
    }
}

Example: Synchronized get() Method

Let’s examine the get() method in the Counter class:

java
public class Counter {
    private int count;

    public int get() {
        return count;
    }
    // ...
}

This method does not need synchronization because reading a single int variable does not require synchronization.

However, if we modify the code to return an object containing two int variables:

java
public class Counter {
    private int first;
    private int last;

    public Pair get() {
        Pair p = new Pair();
        p.first = first;
        p.last = last;
        return p;
    }
    // ...
}

Synchronization becomes necessary to ensure that both first and last are read consistently. Without synchronization, another thread might modify first or last while get() is executing, leading to inconsistent state.

Summary

  • Synchronized Methods: Using the synchronized keyword on a method makes the entire method a synchronized block, locking on this for instance methods and on the class's Class instance for static methods.

  • Thread Safety through Encapsulation: By encapsulating synchronized blocks within methods, you ensure that the synchronization logic is maintained internally, promoting cleaner and safer code.

  • Default Thread Safety: Unless explicitly specified, a class is not thread-safe.

  • Synchronization on the Same Lock Object: When using synchronized, it is crucial to lock on the same object to ensure mutual exclusion. Using different lock objects can lead to data inconsistency as threads can enter their respective synchronized blocks simultaneously.

  • Immutable and Static Classes: Immutable classes (e.g., String, Integer, LocalDate) and classes with only static methods (e.g., Math) are inherently thread-safe and do not require synchronization.

  • Consistent Variable Access: Methods that read or write multiple shared variables must synchronize access to ensure that the variables are read and written atomically, preventing logical errors.

  • Equivalence of synchronized Blocks and Methods: Locking on this using a synchronized block within a method is equivalent to declaring the entire method as synchronized.

  • Static Method Synchronization: Synchronizing static methods locks on the class’s Class instance, ensuring that only one thread can execute any synchronized static method at a time across all instances.

By following these principles, you can design thread-safe classes and manage synchronized access to shared resources effectively in your Java programs.

Synchronized Methods has loaded