Java > Concurrency and Multithreading > Thread Basics > Thread Synchronization

Synchronized Block for Thread Safety

This snippet demonstrates thread synchronization using a synchronized block to protect critical sections of code from concurrent access. This ensures data consistency when multiple threads are modifying shared resources.

Code Snippet

This code defines a `SynchronizedCounter` class with a private `count` variable and a `lock` object. The `increment()` and `getCount()` methods use a `synchronized` block with the `lock` object to ensure that only one thread can access and modify the `count` variable at a time. The `main` method creates two threads that increment the counter, and the final count is printed after both threads have finished executing. Without the `synchronized` block, the final count would likely be incorrect due to race conditions.

public class SynchronizedCounter {

    private int count = 0;
    private final Object lock = new Object();

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

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

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

        Runnable task = () -> {
            for (int i = 0; i < 10000; i++) {
                counter.increment();
            }
        };

        Thread thread1 = new Thread(task);
        Thread thread2 = new Thread(task);

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

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

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

Concepts Behind the Snippet

Thread synchronization is crucial for preventing race conditions and ensuring data consistency when multiple threads access shared resources concurrently. The `synchronized` keyword in Java provides a mechanism for creating critical sections of code that can only be accessed by one thread at a time. This is achieved by associating a lock with each object. When a thread enters a `synchronized` block, it acquires the lock for the object. If another thread tries to enter a `synchronized` block for the same object, it will be blocked until the first thread releases the lock.

Real-Life Use Case

Imagine a banking application where multiple threads are trying to update the balance of a bank account concurrently. Without proper synchronization, it's possible for two threads to read the balance at the same time, perform different operations, and then write back incorrect values. This could lead to financial discrepancies and data corruption. Using synchronized blocks or methods ensures that only one thread can update the account balance at a time, preventing these issues.

Best Practices

  • Minimize the Scope of Synchronized Blocks: Keep the synchronized blocks as small as possible to reduce the time threads spend waiting for the lock. This improves overall performance.
  • Use a Dedicated Lock Object: Using a separate lock object (like the `lock` field in the example) provides more flexibility and control compared to synchronizing on the object itself (using `synchronized(this)`).
  • Avoid Holding Locks for Long Operations: Do not perform long-running operations (e.g., network calls, disk I/O) within synchronized blocks, as this can cause other threads to wait unnecessarily.

Interview Tip

When discussing thread synchronization in interviews, be prepared to explain the concepts of race conditions, critical sections, and the different mechanisms for achieving synchronization in Java (e.g., `synchronized` keyword, `Lock` interface, `Semaphore`, `CountDownLatch`). Be able to describe the advantages and disadvantages of each approach and when to use them.

When to Use Them

Use synchronized blocks whenever multiple threads are accessing and modifying shared mutable state. If the data is immutable, synchronization may not be necessary. Also consider using other synchronization constructs like `ReentrantLock` or concurrent collections from `java.util.concurrent` for more advanced synchronization scenarios.

Memory Footprint

The memory footprint of synchronized blocks is relatively small. Each object in Java has an associated monitor, which is used for synchronization. The monitor adds a small overhead to the object's memory footprint. The main overhead comes from the potential blocking of threads waiting to acquire the lock, which can consume system resources. Also the dedicated lock object consume some memory too.

Alternatives

Alternatives to synchronized blocks include:

  • ReentrantLock: A more flexible lock implementation that provides features like fair locking and interruptible waiting.
  • ReadWriteLock: Allows multiple readers or a single writer to access a shared resource concurrently.
  • Concurrent Collections: Data structures like `ConcurrentHashMap` and `CopyOnWriteArrayList` provide built-in thread safety and can eliminate the need for explicit synchronization.
  • Atomic Variables: Classes like `AtomicInteger` and `AtomicReference` provide atomic operations on primitive types and object references.

Pros

  • Simplicity: The `synchronized` keyword is easy to use and understand.
  • Built-in Support: It is a fundamental part of the Java language and JVM.
  • Guaranteed Mutual Exclusion: Ensures that only one thread can access the synchronized block at a time.

Cons

  • Performance Overhead: Synchronized blocks can introduce performance overhead due to lock contention and thread blocking.
  • Limited Flexibility: Lacks some of the advanced features offered by other synchronization mechanisms, such as fair locking and interruptible waiting.
  • Risk of Deadlock: Improper use of synchronized blocks can lead to deadlocks, where two or more threads are blocked indefinitely, waiting for each other to release locks.

FAQ

  • What is a race condition?

    A race condition occurs when multiple threads access and modify shared data concurrently, and the final outcome depends on the unpredictable order of execution. This can lead to data corruption and unexpected behavior.
  • What is a critical section?

    A critical section is a section of code that accesses shared resources and must be protected from concurrent access by multiple threads. Synchronized blocks are used to define critical sections.
  • Why use a dedicated lock object instead of `synchronized(this)`?

    Using a dedicated lock object provides more control and flexibility. It allows you to synchronize access to specific parts of your code without synchronizing the entire object. Also, it prevents external code from accidentally synchronizing on your object and interfering with your synchronization strategy.