Why is Synchronization Needed ?

About

Synchronization is essential in multithreaded programming to ensure data integrity, consistency, and predictable execution. Without proper synchronization, concurrent access to shared resources can lead to race conditions, inconsistent data, deadlocks, and other critical issues. Below are the key reasons why synchronization is necessary, along with examples.

1. Avoid Race Conditions

What is a Race Condition?

A race condition occurs when multiple threads try to access and modify the same resource at the same time, leading to unpredictable results. The final outcome depends on the exact timing of thread execution, making the program behavior inconsistent.

Example Without Synchronization (Race Condition)

class Counter {
    private int count = 0;

    void increment() {
        count++;  // Not atomic, can cause issues
    }

    int getCount() {
        return count;
    }
}

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

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

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

        t1.start();
        t2.start();
        t1.join();
        t2.join();

        System.out.println("Final Count: " + counter.getCount());  // Expected: 20000, but might be less!
    }
}

Problem

  • Multiple threads update count simultaneously.

  • Due to context switching, some increments are lost.

  • The final count is inconsistent and unpredictable.

Fix Using Synchronization

Now, increment() is synchronized, preventing data corruption.

2. Ensure Data Consistency

What is Data Inconsistency?

When multiple threads modify shared data without proper synchronization, it may lead to an inconsistent state where values become invalid or out of sync.

Example Without Synchronization (Inconsistent Data)

Problem

  • printValues() may execute before updateValues() finishes, leading to invalid output like x: 10, y: 0.

Fix Using Synchronization

Now, updateValues() and printValues() execute sequentially, ensuring data consistency.

3. Maintain Order of Execution

What is Execution Order Issue?

In a multithreaded environment, operations may execute out of order, leading to unintended behavior.

Example Without Synchronization (Out-of-Order Execution)

Problem

  • checkFlag() may execute before setFlag(), printing "Flag is not set!", which is incorrect.

Fix Using Synchronization

Now, thread execution order is controlled.

4. Prevent Deadlocks and Starvation

Deadlock

Occurs when two or more threads wait indefinitely for each other's locks.

Example of Deadlock

In this example, the deadlock occurs because each thread is holding a lock that the other thread needs, and neither thread can proceed until the other thread releases its lock

Problem

  • Resource Class: The Resource class has a method methodA that takes another Resource object as a parameter. Inside methodA, it first acquires a lock on the current Resource object (this) and then tries to acquire a lock on the passed Resource object (r).

  • DeadlockExample Class: In the main method, two Resource objects (r1 and r2) are created. Two threads (t1 and t2) are started:

    • t1 calls r1.methodA(r2), which means t1 will first lock r1 and then try to lock r2.

    • t2 calls r2.methodA(r1), which means t2 will first lock r2 and then try to lock r1.

  • Potential Deadlock: If t1 locks r1 and t2 locks r2 at the same time, t1 will be waiting for r2 to be unlocked, and t2 will be waiting for r1 to be unlocked. This situation causes a deadlock because both threads are waiting for each other to release the locks, and neither can proceed.

Fix: Avoid Nested Locks & Use Try-Lock

To avoid potential deadlocks, we can use tryLock with a timeout. This way, if a thread cannot acquire the lock within the specified time, it will give up and avoid deadlock.

Using tryLock(), a thread will not wait indefinitely for a lock.

  • Lock Initialization: Each Resource object has its own ReentrantLock instance.

  • Method methodA: The method attempts to acquire a lock on the current Resource object (this) using tryLock with a timeout of 1 second. If the lock is acquired within the timeout, it proceeds to the next steps; otherwise, it prints a message and exits.

  • Simulate Work: Once the lock on the current Resource is acquired, it simulates some work by calling Thread.sleep(100).

  • Nested Lock Attempt: The method then attempts to acquire a lock on the passed Resource object (r) using tryLock with a timeout of 1 second. If the lock is acquired, it prints a message and releases the lock in the finally block. If the lock is not acquired within the timeout, it prints a message indicating the failure to lock the second resource.

  • Exception Handling: The method catches and handles InterruptedException that might be thrown by Thread.sleep.

Last updated