Ora

How does Java synchronized work?

Published in Java Concurrency 6 mins read

Java's synchronized keyword is a fundamental mechanism for ensuring thread safety, acting as a powerful tool to prevent race conditions by controlling access to shared resources. It essentially locks a method or a specific block of code, ensuring that only one thread can execute it at a time.

Understanding Java's Synchronized Mechanism

At its core, synchronized is about mutual exclusion and memory visibility, critical aspects of concurrent programming. When multiple threads try to access and modify the same shared data simultaneously, it can lead to inconsistent and unpredictable results – known as race conditions. The synchronized keyword provides a way to establish a "critical section" where shared data can be accessed safely.

How Synchronized Works Under the Hood: Intrinsic Locks (Monitors)

Every object in Java inherently has an associated intrinsic lock, also known as a monitor. When a thread enters a synchronized block or method, it attempts to acquire this lock.

  1. Acquiring the Lock: If the lock is available, the thread acquires it and proceeds to execute the synchronized code.
  2. Blocking Other Threads: While one thread holds the lock, any other thread attempting to enter the same synchronized block or method (protected by the same lock object) will be blocked and put into a waiting state until the lock is released.
  3. Releasing the Lock: The lock is automatically released when the thread exits the synchronized block or method, either normally (after completion) or abnormally (due to an exception).

This mechanism guarantees that only one thread can execute the protected code at any given moment, thus preventing data corruption from concurrent access.

Types of Synchronized Constructs

The synchronized keyword can be applied in two primary ways:

1. Synchronized Methods

When you declare a method as synchronized, the entire method body becomes a critical section.

  • Non-Static Methods: For synchronized non-static methods, the lock acquired is the instance lock of the object on which the method is called. This means that if obj1.synchronizedMethod() is called, the lock is on obj1. No other thread can call any other synchronized non-static method on obj1 until the first thread releases the lock. However, other threads can call synchronized methods on different instances (obj2.synchronizedMethod()).

    public class Counter {
        private int count = 0;
    
        public synchronized void increment() {
            count++; // This entire method is synchronized on 'this' object
        }
    
        public synchronized int getCount() {
            return count; // Also synchronized on 'this' object
        }
    }
  • Static Methods: For synchronized static methods, the lock acquired is the class lock of the class to which the method belongs. This means that only one thread can execute any static synchronized method of that class at a time, regardless of how many instances exist.

    public class Logger {
        private static String log = "";
    
        public static synchronized void writeLog(String message) {
            log += message + "\n"; // This method is synchronized on Logger.class
        }
    }

2. Synchronized Blocks

You can also specify a smaller block of code to be synchronized, which often provides more fine-grained control and can improve concurrency. To do this, you must explicitly provide an object that will serve as the lock.

public class DataProcessor {
    private Object lock = new Object(); // A dedicated lock object
    private int data = 0;

    public void processData() {
        // ... some non-critical code ...
        synchronized (lock) { // Synchronized block on 'lock' object
            data++; // This critical section is protected
        }
        // ... more non-critical code ...
    }

    public void retrieveData() {
        synchronized (lock) { // This also uses the 'lock' object
            System.out.println("Data: " + data);
        }
    }
}

Key Properties of Synchronized

  • Mutual Exclusion: Only one thread can execute a synchronized method or block protected by the same lock at any given time. This is the primary mechanism to prevent race conditions.
  • Memory Visibility: Beyond mutual exclusion, synchronized also guarantees memory visibility. When a thread releases a monitor, a "happens-before" relationship is established, ensuring that all writes made by that thread before releasing the lock are visible to any other thread that subsequently acquires the same lock. This prevents stale data issues.
  • Reentrancy: Java's intrinsic locks are reentrant. This means that if a thread has already acquired a lock, it can re-enter any other synchronized block or method that is protected by the same lock without deadlocking itself. For example, if a synchronized method calls another synchronized method on the same object, the thread doesn't have to re-acquire the lock; it already holds it.

When to Use Synchronized (Practical Scenarios)

synchronized is ideal for situations where:

  • Shared Mutable State: You have variables or data structures that are accessed and modified by multiple threads (e.g., counters, shared lists, caches).
  • Critical Sections: You need to define a specific block of code that must be executed by only one thread at a time to maintain data integrity.
  • Simple Thread Safety: For straightforward scenarios where complex concurrency constructs (like java.util.concurrent.locks.Lock interface) might be overkill.

Comparison: Synchronized Method vs. Synchronized Block

Feature Synchronized Method Synchronized Block
Lock Object this (for non-static), Class.class (for static) Any object explicitly specified in synchronized(obj)
Granularity Entire method is locked. Only a specific section of code is locked.
Flexibility Less flexible; locks the whole method. More flexible; allows locking only critical sections.
Concurrency May reduce concurrency more if non-critical code is within the method. Can improve concurrency by locking only necessary parts.
Readability Simpler syntax, can be easier to read for full method protection. Slightly more verbose, but clearly delimits the critical section.

Potential Considerations and Best Practices

  • Performance Overhead: synchronized operations involve some overhead due due to lock acquisition and release. Overuse can lead to performance bottlenecks.
  • Deadlocks: Incorrect use of synchronized (e.g., acquiring locks in different orders across multiple threads) can lead to deadlocks, where threads endlessly wait for each other to release a lock.
  • Starvation: A thread might repeatedly lose the race to acquire a lock, potentially never getting to execute its critical section.
  • Granularity: Prefer synchronized blocks over synchronized methods when only a small portion of the method requires protection. This allows other parts of the method to execute concurrently, improving throughput.
  • Lock Objects: When using synchronized blocks, it's generally good practice to synchronize on a private, final object that is not exposed to other parts of the code. This prevents external code from acquiring your lock and potentially causing unexpected behavior or deadlocks.

By understanding these principles, developers can effectively leverage synchronized to build robust and thread-safe Java applications.