Skip to content

Lesson 2: Synchronization

Overview:

Lesson 2 Module 7: Multithreading-Synchronization focuses on ensuring thread safety in Java through synchronization. When multiple threads access shared resources, there is a need to synchronize their access to prevent data corruption or inconsistent results. This lesson covers synchronized methods and blocks, essential mechanisms for controlling access to critical sections in a multithreaded environment.

Key Concepts:

  1. Thread Safety:
    • Definition: Thread safety ensures that a program behaves correctly when multiple threads are executing concurrently.
    • Purpose: Prevents data corruption and ensures consistent results when multiple threads access shared resources.
  2. Synchronized Methods:
    • Definition: Synchronized methods are methods that are controlled by locks and can only be accessed by one thread at a time.
    • Usage: Use the synchronized keyword in the method signature to make the entire method synchronized.
  3. Synchronized Blocks:
    • Definition: Synchronized blocks are code blocks that are controlled by locks and can only be executed by one thread at a time.
    • Usage: Use the synchronized keyword followed by an object reference (lock) within a code block to synchronize that block.
  4. Intrinsic Locks and Reentrancy:
    • Intrinsic Locks (Monitors):
      • In Java, every object has an intrinsic lock associated with it. Synchronized methods and blocks use this lock.
    • Reentrancy:
      • A thread that already holds a lock can acquire it again without blocking.

Example:

Let’s create a program that demonstrates the need for synchronization and uses both synchronized methods and blocks to ensure thread safety.

class Counter {
    private int count = 0;

    // Synchronized method
    public synchronized void increment() {
        for (int i = 0; i < 5; i++) {
            count++;
            System.out.println(Thread.currentThread().getName() + " - Incremented Count: " + count);
            try {
                Thread.sleep(100); // Simulate some work
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    // Synchronized block
    public void decrement() {
        synchronized (this) {
            for (int i = 0; i < 5; i++) {
                count--;
                System.out.println(Thread.currentThread().getName() + " - Decremented Count: " + count);
                try {
                    Thread.sleep(100); // Simulate some work
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

public class SynchronizationExample {

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

        // Creating threads
        Thread incrementThread = new Thread(() -> counter.increment());
        Thread decrementThread = new Thread(() -> counter.decrement());

        // Starting threads
        incrementThread.start();
        decrementThread.start();

        // Waiting for threads to complete
        try {
            incrementThread.join();
            decrementThread.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println("Main Thread - Execution Completed");
    }
}
  1. Counter Class:
    • Counter class contains a private count variable that will be manipulated by multiple threads.
    • increment() method is synchronized using the synchronized keyword in the method signature, ensuring only one thread can execute this method at a time.
    • decrement() method uses a synchronized block with synchronized (this) to achieve the same effect.
  2. SynchronizationExample Class:
    • In the main method, an instance of the Counter class is created.
    • Two threads (incrementThread and decrementThread) are created with lambda expressions to execute the increment() and decrement() methods, respectively.
    • Both threads are started concurrently using the start() method.
  3. Thread Execution:
    • Both threads execute their respective methods concurrently, and due to synchronization, they take turns accessing the shared count variable, preventing race conditions.
  4. Joining Threads:
    • The join() method is used to wait for both threads to complete their execution before moving on with the main thread.
  5. Output:
    • The program outputs the incremented and decremented counts, demonstrating the synchronization of access to the shared variable.

Understanding the output helps visualize how synchronization ensures that the shared resource (count variable) is accessed in a thread-safe manner, preventing data corruption.