Java tutorials > Multithreading and Concurrency > Threads and Synchronization > How to synchronize access to shared resources (`synchronized`)?

How to synchronize access to shared resources (`synchronized`)?

This tutorial explains how to synchronize access to shared resources in Java using the `synchronized` keyword. Synchronization is crucial for preventing race conditions and ensuring data integrity when multiple threads access the same data.

Introduction to Synchronization

In a multithreaded environment, multiple threads can execute concurrently. If these threads access shared resources (e.g., variables, files, databases) without proper coordination, it can lead to data corruption and inconsistent results. This is known as a race condition. Synchronization mechanisms, such as the `synchronized` keyword in Java, provide a way to control access to shared resources and prevent these issues.

The `synchronized` Keyword

The `synchronized` keyword in Java is used to create critical sections in your code. A critical section is a block of code that can only be executed by one thread at a time. When a thread enters a `synchronized` block or method, it acquires a lock associated with the object or class being synchronized on. Other threads attempting to enter the same `synchronized` block or method will be blocked until the lock is released.

Synchronized Methods

Synchronizing a method ensures that only one thread can execute that method at a time. In this example, the `increment()` and `getCount()` methods of the `Counter` class are synchronized. This means that if one thread is executing `increment()`, no other thread can execute `increment()` or `getCount()` until the first thread has finished executing `increment()`. When a method is declared `synchronized`, the lock associated with the object (instance of `Counter` in this case) is acquired when the method is called and released when the method returns.

public class Counter {
    private int count = 0;

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

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

Synchronized Blocks

Synchronized blocks provide finer-grained control over synchronization. You can specify the object to lock on using the `synchronized` keyword followed by parentheses containing the object reference. Only one thread can execute the code within the `synchronized` block at a time, while other threads are blocked waiting for the lock to be released. In this example, a separate `lock` object is used for synchronization. This is useful when you want to synchronize access to specific parts of a method rather than the entire method. Using a separate lock object can improve performance compared to synchronizing the entire method, especially if the method contains sections of code that don't require synchronization. The object specified in the `synchronized` block is crucial. If multiple blocks synchronize on different objects, they will not block each other.

public class Counter {
    private int count = 0;
    private final Object lock = new Object();

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

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

Concepts Behind the Snippet

The core concept behind `synchronized` is mutual exclusion. Mutual exclusion ensures that only one thread can access a shared resource at any given time, preventing race conditions and data corruption. The `synchronized` keyword achieves mutual exclusion by using locks. Each Java object has an associated lock (also known as a monitor). When a thread enters a `synchronized` block or method, it must acquire the lock associated with the object or class. If the lock is already held by another thread, the thread will block until the lock is released.

Real-Life Use Case Section

Consider a bank account where multiple threads (e.g., representing different users or transactions) might try to deposit or withdraw money simultaneously. Without synchronization, it's possible for two threads to read the current balance, both add their respective amounts, and then write the result back, leading to an incorrect balance. Using `synchronized` methods or blocks ensures that only one transaction updates the balance at a time, maintaining data integrity.

Best Practices

  • Minimize the scope of synchronized blocks: Only synchronize the code sections that absolutely require mutual exclusion. Synchronizing larger blocks of code can reduce concurrency and impact performance.
  • Avoid holding locks for long periods: Holding locks for extended durations can cause other threads to wait unnecessarily. Release the lock as soon as the critical section is completed.
  • Use separate lock objects when appropriate: Consider using separate lock objects for different shared resources to avoid unnecessary blocking and improve concurrency.
  • Avoid deadlocks: Be careful when using multiple locks to prevent deadlocks, where two or more threads are blocked indefinitely, waiting for each other to release locks.
  • Prefer higher-level concurrency utilities: For more complex concurrency scenarios, consider using the `java.util.concurrent` package, which provides higher-level concurrency utilities like `ReentrantLock`, `Semaphore`, and `ConcurrentHashMap`. These utilities often offer more flexibility and control compared to `synchronized`.

Interview Tip

When discussing synchronization in interviews, be prepared to explain the concepts of race conditions, mutual exclusion, and the role of locks. Demonstrate your understanding of the `synchronized` keyword and its use in both methods and blocks. Be ready to discuss the trade-offs between using `synchronized` and other concurrency mechanisms. You should also understand the potential for deadlocks and how to avoid them.

When to Use Them

Use `synchronized` when you need to ensure that only one thread accesses a shared resource at a time to prevent data corruption or race conditions. It is especially useful for simple synchronization needs within a single object. However, for more complex concurrency requirements, consider using the classes in `java.util.concurrent`.

Memory Footprint

The `synchronized` keyword has a relatively small memory footprint. Each Java object has an associated monitor (lock), but this monitor only consumes memory when it's actively being used (i.e., when a thread holds the lock). The overhead is typically minimal. However, excessive synchronization can impact performance by introducing contention and blocking threads.

Alternatives

Alternatives to `synchronized` include:

  • `ReentrantLock`: A more flexible locking mechanism that provides features such as fairness and the ability to interrupt waiting threads.
  • `Semaphore`: Controls access to a limited number of resources.
  • `CountDownLatch`: Allows one or more threads to wait until a set of operations being performed in other threads completes.
  • `ConcurrentHashMap`: A thread-safe implementation of the `HashMap` interface.
  • Atomic Variables (e.g., `AtomicInteger`, `AtomicLong`): Provide atomic operations for simple data types, eliminating the need for explicit locking in some cases.

Pros

  • Simplicity: `synchronized` is relatively easy to use and understand.
  • Built-in: It's a built-in language feature, so no external libraries are required.
  • Automatic lock management: The JVM automatically handles lock acquisition and release, reducing the risk of errors.

Cons

  • Limited flexibility: `synchronized` provides limited control over locking behavior compared to other concurrency mechanisms.
  • Potential for deadlocks: Improper use of `synchronized` can lead to deadlocks.
  • Performance overhead: Excessive synchronization can impact performance.
  • No fairness guarantee: There is no guarantee that threads will acquire the lock in any particular order.

FAQ

  • What happens if I try to synchronize on `null`?

    Attempting to synchronize on `null` will throw a `NullPointerException`.
  • Is it possible to synchronize on a primitive type?

    No, you cannot synchronize on a primitive type directly. You can synchronize on the corresponding wrapper object (e.g., `Integer`, `Boolean`). However, be aware that if multiple threads use the same boxed `Integer` object (e.g., due to autoboxing of the same integer value), they might inadvertently synchronize on the same object when they didn't intend to.
  • What is the difference between `synchronized` and `ReentrantLock`?

    `synchronized` is a built-in language feature that provides basic locking functionality. `ReentrantLock` is a class in the `java.util.concurrent` package that offers more flexibility and control, such as fairness, the ability to interrupt waiting threads, and the ability to try to acquire a lock without blocking.