Java > Concurrency and Multithreading > Synchronization and Locks > Synchronized Methods and Blocks

Synchronized Methods: Controlling Access to Shared Resources

This code snippet demonstrates how to use synchronized methods in Java to control access to shared resources, preventing race conditions and ensuring data consistency in a multithreaded environment.

Code Snippet: Synchronized Method Example

This code defines a `Counter` class with a `count` variable. The `increment()` and `getCount()` methods are synchronized. When a thread calls a synchronized method on an object, it acquires the lock associated with that object. No other thread can execute any synchronized method on the same object until the first thread releases the lock. This prevents multiple threads from modifying the `count` variable simultaneously, avoiding race conditions.

class Counter {
    private int count = 0;

    // Synchronized method
    public synchronized void increment() {
        count++;
    }

    public synchronized int getCount() {
        return count;
    }
}

class SynchronizedMethodExample {
    public static void main(String[] args) throws InterruptedException {
        Counter counter = new Counter();

        Thread thread1 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                counter.increment();
            }
        });

        Thread thread2 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                counter.increment();
            }
        });

        thread1.start();
        thread2.start();

        thread1.join();
        thread2.join();

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

Concepts Behind Synchronized Methods

  • Intrinsic Lock (Monitor): Every Java object has an intrinsic lock, also known as a monitor. Synchronized methods implicitly acquire and release this lock.
  • Mutual Exclusion: Only one thread can hold the lock for an object at a time, ensuring mutual exclusion.
  • Visibility: Changes made to variables by a thread holding the lock are visible to subsequent threads that acquire the lock.

Real-Life Use Case

Consider a banking application where multiple threads might try to update the balance of a shared account simultaneously. Using synchronized methods on methods that modify the account balance (e.g., deposit, withdraw) can prevent overdrafts or incorrect balances due to race conditions.

Best Practices

  • Minimize Scope: Only synchronize the methods or blocks of code that actually need to be protected. Excessive synchronization can lead to performance bottlenecks.
  • Avoid Long Operations: Don't perform lengthy operations inside synchronized blocks. Keep the synchronized section as short as possible.
  • Use `synchronized` only when necessary: Understand the thread safety requirements of your classes before using `synchronized`. Often, thread-safe data structures from the `java.util.concurrent` package provide better performance.

Interview Tip

Be prepared to explain how `synchronized` works, the concept of intrinsic locks, and the potential for deadlocks when using multiple locks. Also be ready to discuss the performance implications of `synchronized` and alternatives such as `java.util.concurrent` classes like `AtomicInteger` or `ReentrantLock`.

When to Use Synchronized Methods

Use synchronized methods when you need to ensure that only one thread at a time can access and modify the state of an object. It is a simple and effective way to achieve thread safety for simple scenarios.

Memory Footprint

Synchronized methods themselves don't significantly increase the memory footprint. The memory overhead is primarily related to the object's monitor, which is a small amount of metadata associated with each object in the JVM. However, excessive locking can indirectly affect performance and thus resource consumption (CPU, memory due to increased context switching).

Alternatives

  • Atomic Variables: Classes like `AtomicInteger` and `AtomicLong` provide atomic operations that can be used instead of synchronized blocks for simple operations like incrementing or decrementing a counter.
  • ReentrantLock: Provides more flexibility and control over locking compared to synchronized methods.
  • ReadWriteLock: Allows multiple readers to access a resource concurrently but only one writer at a time.
  • Concurrent Collections: Classes like `ConcurrentHashMap` and `ConcurrentLinkedQueue` are designed for concurrent access and offer better performance than synchronizing access to standard collections.

Pros

  • Simple to use: `synchronized` is a keyword directly integrated into the Java language, making it easy to understand and implement.
  • Built-in: No need for external libraries or dependencies.
  • Guaranteed mutual exclusion: Ensures that only one thread can access the synchronized block or method at a time.

Cons

  • Performance overhead: Synchronization can introduce performance overhead due to lock contention and context switching.
  • Potential for deadlocks: If multiple threads acquire locks in different orders, it can lead to deadlocks.
  • Limited flexibility: Synchronized methods and blocks offer limited control compared to more advanced locking mechanisms like `ReentrantLock`.

Synchronized Blocks: Finer-Grained Control

This example uses a synchronized block to protect a critical section of code. Instead of synchronizing the entire method, only the block that modifies the `counter` is synchronized. This allows other parts of the `performTask()` method to execute concurrently, potentially improving performance. The `lock` object is used as the monitor for the synchronized block. Any object can be used as a lock.

class SharedResource {
    private int counter = 0;
    private final Object lock = new Object();

    public void performTask() {
        // Some unsynchronized operations
        System.out.println("Thread " + Thread.currentThread().getName() + ": Before synchronized block");

        synchronized (lock) {
            // Critical section: Only one thread can execute this block at a time
            System.out.println("Thread " + Thread.currentThread().getName() + ": Inside synchronized block");
            counter++;
            // Simulate some work
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
            System.out.println("Thread " + Thread.currentThread().getName() + ": Counter incremented to " + counter);
        }

        // Some more unsynchronized operations
        System.out.println("Thread " + Thread.currentThread().getName() + ": After synchronized block");
    }

    public int getCounter() {
        return counter;
    }
}


public class SynchronizedBlockExample {
    public static void main(String[] args) throws InterruptedException {
        SharedResource resource = new SharedResource();

        Thread thread1 = new Thread(() -> {
            for (int i = 0; i < 5; i++) {
                resource.performTask();
            }
        }, "Thread-1");

        Thread thread2 = new Thread(() -> {
            for (int i = 0; i < 5; i++) {
                resource.performTask();
            }
        }, "Thread-2");

        thread1.start();
        thread2.start();

        thread1.join();
        thread2.join();

        System.out.println("Final Counter Value: " + resource.getCounter()); // Expected: 10
    }
}

FAQ

  • What happens if I synchronize on `this`?

    Synchronizing on `this` is equivalent to synchronizing the entire method if the synchronized block encompasses the entire method body. It means the intrinsic lock of the object on which the method is called will be acquired.
  • Can I synchronize on a `null` object?

    No, attempting to synchronize on a `null` object will throw a `NullPointerException`.
  • How does synchronization prevent race conditions?

    Synchronization ensures that only one thread can execute the synchronized code at a time. This prevents multiple threads from accessing and modifying shared data concurrently, which eliminates race conditions and ensures data consistency.
  • What is a deadlock, and how can it be avoided?

    A deadlock occurs when two or more threads are blocked indefinitely, waiting for each other to release locks. To avoid deadlocks, ensure that threads acquire locks in a consistent order and avoid holding multiple locks for extended periods. Use techniques like lock ordering or timeouts to prevent deadlocks.