Skip to main content
Java Multithreading and Concurrency

Thread Safety and Synchronization in Java

Aakash Verma

What is Synchronization in Java?

Synchronization in Java is a way to control access to shared resources in a multi-threaded environment. When multiple threads try to access the same resource (like a variable or a method), synchronization ensures that only one thread can access that resource at a time. This prevents race conditions, where the outcome of operations depends on the unpredictable timing of threads.

The synchronized Keyword

Java provides the synchronized keyword to help manage thread access. When you use synchronized, you are telling Java to lock the resource so only one thread can access it at a time.

Monitor Locks

Every object in Java has an associated monitor lock (often just called a "monitor"). When a thread enters a synchronized block or method, it acquires the monitor for the object. This means no other thread can enter any synchronized method/block on that object until the first thread releases the monitor by exiting the synchronized block/method.

Example: Synchronizing Methods on this Object

Let's consider a class Employee:

class Employee {
    private String name;

    // Method synchronized on 'this' object
    public synchronized void setName(String name) {
        this.name = name;
    }

    // Another method synchronized on 'this' object
    public synchronized void resetName() {
        this.name = "";
    }

    // Synchronized block inside the method
    // This is equivalent of adding synchronized in method definition
    public String getName() {
        synchronized (this) {  // Explicitly synchronizing on 'this'
            return this.name;
        }
    }
}

Explanation:setName and resetName are synchronized methods, so they automatically synchronize on the current instance of Employee (referred to as this).getName uses a synchronized block, also synchronizing on this. This block only allows one thread to execute it at a time.

Scenario: If three different threads try to call setName, resetName, and getName simultaneously on the same Employee object, only one will proceed at a time, while the others wait.

Synchronizing on a Different Object

Now, let's modify the Employee class to use a different object for synchronization:

class Employee {
    private String name;
    private final Object lock = new Object();

    // Method synchronized on 'this' object
    public synchronized void setName(String name) {
        this.name = name;
    }

    // Another method synchronized on 'this' object
    public synchronized void resetName() {
        this.name = "";
    }

    // Synchronized on a different object 'lock'
    public String getName() {
        synchronized (lock) {  // Synchronizing on a different object
            return this.name;
        }
    }
}

Explanation:setName and resetName are still synchronized on the this object.getName is now synchronized on a different object called lock.

Scenario: If two threads call setName and resetName, they will block each other because they synchronize on the same this object. However, if another thread calls getName, it won’t be blocked by setName or resetName because it synchronizes on a different object (lock).

Monitor Lock Behavior and Re-entrancy

When a thread holds a monitor lock, it can re-enter any synchronized method/block that synchronizes on the same monitor without getting blocked. This is known as re-entrancy.

Common Pitfall: Reassigning a Synchronized Object

class IncorrectSynchronization {

    Boolean flag = new Boolean(true);

    public void example() throws InterruptedException {
        Thread t1 = new Thread(() -> {
            synchronized (flag) {
                try {
                    while (flag) {
                        System.out.println("Thread 1: about to sleep");
                        Thread.sleep(5000);
                        System.out.println("Thread 1: woke up and about to wait");
                        flag.wait();  // Waiting on flag
                    }
                } catch (InterruptedException e) {
                    // Handle exception
                    e.printStackTrace();
                }
            }
        });

        Thread t2 = new Thread(() -> {
            flag = false;  // Reassigning the flag object
            System.out.println("Thread 2: Boolean reassigned.");
        });

        t1.start();
        Thread.sleep(1000);  // Let t1 acquire the lock first
        t2.start();

        t1.join();
        t2.join();
    }

    public static void main(String[] args) throws InterruptedException {
        IncorrectSynchronization incorrectSync = new IncorrectSynchronization();
        incorrectSync.example();
    }
}

Explanation:

    • Thread t1 synchronizes on flag and then goes to sleep.
    • Thread t2 reassigns flag to a new Boolean object while t1 is asleep.
    • When t1 wakes up and tries to call wait() on flag, it is now pointing to a different object, causing an IllegalMonitorStateException.

Correct Method

class CorrectSynchronization {

    // Use a separate, immutable lock object
    private final Object lock = new Object();
    private boolean flag = true; 

    public void example() throws InterruptedException {
        Thread t1 = new Thread(() -> {
            synchronized (lock) {  // Synchronize on the lock object
                try {
                    while (flag) {
                        System.out.println("Thread 1: about to sleep");
                        Thread.sleep(5000);
                        System.out.println("Thread 1: woke up and about to wait");
                        lock.wait();  // Wait on the lock object
                    }
                } catch (InterruptedException e) {
                    // Handle exception
                    e.printStackTrace();
                }
            }
        });

        Thread t2 = new Thread(() -> {
            synchronized (lock) {  // Synchronize on the same lock object
                flag = false;  // Update the flag
                System.out.println("Thread 2: flag set to false.");
                lock.notify();  // Notify waiting threads
            }
        });

        t1.start();
        Thread.sleep(1000);  // Let t1 acquire the lock first
        t2.start();

        t1.join();
        t2.join();
    }

    public static void main(String[] args) throws InterruptedException {
        CorrectSynchronization correctSync = new CorrectSynchronization();
        correctSync.example();
    }
}

Key Changes and Explanation:

  1. Separate Lock Object: Introduced a separate, immutable lock object (private final Object lock = new Object();) used solely for synchronization. This object is never reassigned, so it remains consistent across all synchronized blocks.
  2. Primitive Flag: The flag variable is now a primitive boolean instead of a Boolean object. This eliminates the risk of accidentally reassigning the flag object and causing synchronization issues.
  3. Synchronization on Lock Object: Both threads synchronize on the same lock object, ensuring that they properly coordinate their actions.
  4. Notification of Waiting Threads: t2 calls lock.notify() after updating flag, which wakes up any thread that is waiting on lock (in this case, t1).

Benefits:

  • Consistency: The lock object (lock) is never reassigned, ensuring that all threads synchronize on the same object.
  • Thread Safety: The flag is safely updated and checked within a synchronized block, preventing race conditions and ensuring proper coordination between threads.

Key Takeaways

  • synchronized keyword ensures that only one thread can access a block/method at a time.
  • Monitor locks are automatically managed by Java when you use synchronized.
  • Be careful not to change the object you're synchronizing on, as it can lead to exceptions.