Multithreading

What is Synchronization

Spread the love

Synchronization that ensures controlled access to shared resources or critical sections of code by multiple threads. It prevents multiple threads from simultaneously modifying or accessing shared data, which could otherwise lead to race conditions, data inconsistency, or corruption. Synchronization ensures thread safety by allowing only one thread to access a critical section at a time.

When to Use Synchronization?

Shared Resources: Multiple threads access or modify a shared resource (e.g., a variable, file, or database).

  • Example: Updating a global counter or writing to the same file.

Race Conditions: There’s a risk of threads interfering with each other due to non-atomic operations.

  • Example: Operations like counter++ (read-modify-write) need synchronization to ensure accuracy.

Thread Coordination: Threads need to communicate or depend on each other in a specific sequence.

  • Example: A producer-consumer problem where one thread produces data and another consumes it.

Critical Sections: A section of the code must be executed by only one thread at a time.

  • Example: Handling transactions in financial applications to ensure consistency.

What is race condition

A race condition occurs in a multithreaded or concurrent program when two or more threads try to access and modify shared data at the same time without proper synchronization. The outcome depends on the order in which threads execute, which can lead to unpredictable and incorrect results.

Imagine two threads incrementing a shared counter variable at the same time:

class RaceConditionDemo {
    private int counter = 0;

    public void increment() {
        counter++; // This operation involves read-modify-write steps
    }

    public int getCounter() {
        return counter;
    }

    public static void main(String[] args) {
        RaceConditionDemo demo = new RaceConditionDemo();

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

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

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

        try {
            thread1.join();
            thread2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println("Final Counter Value: " + demo.getCounter());
    }
}
What is volatile.

The volatile keyword in Java is used to modify a variable to ensure its visibility across threads. It guarantees that changes made to a volatile variable by one thread are immediately visible to all other threads. However, it does not guarantee atomicity, meaning it doesn’t prevent race conditions for compound actions like counter++.

Purpose of volatile

The primary purpose of volatile is to:

  1. Ensure Visibility: When a thread modifies a volatile variable, the updated value is immediately written to the main memory, and other threads always see the latest value.
  2. Prevent Caching Issues: Threads sometimes store variables in CPU caches, and without volatile, a thread may read a stale value from its cache instead of the latest value from main memory.
When to Use volatile

volatile should be used when:

  1. Simple State Updates:
    • When a variable is shared across threads and each thread only reads or writes its value, without depending on the previous state.
    • Example: A volatile flag variable used to signal a thread to stop or start.
  2. No Compound Actions:
    • When there are no compound operations (like x++ or x += 1), as these are not atomic and volatile cannot prevent race conditions for such actions.
  3. Lightweight Alternative:
    • It can be a simpler alternative to synchronization when only visibility (not atomicity) is required.
Non-Volatile Counter Example

Without volatile, a counter variable may not reflect the latest changes made by other threads due to caching by the JVM or CPU.

class NonVolatileCounter {
    private int counter = 0;

    public void increment() {
        counter++; // Increment operation is NOT atomic
    }

    public int getCounter() {
        return counter;
    }
}

public class NonVolatileExample {
    public static void main(String[] args) {
        NonVolatileCounter nvc = new NonVolatileCounter();

        // Create multiple threads to increment the counter
        Runnable task = () -> {
            for (int i = 0; i < 1000; i++) {
                nvc.increment();
            }
        };

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

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

        try {
            thread1.join();
            thread2.join();
            thread3.join();
        } catch (InterruptedException e) {
            System.out.println("Thread interrupted: " + e.getMessage());
        }

        // Print final counter value
        System.out.println("Final Counter Value (non-volatile): " + nvc.getCounter());
    }
}

Issues with the above code ,Threads may use a cached version of counter, leading to inconsistent results.

Volatile Counter Example

The volatile keyword ensures visibility of changes to variables across threads. It guarantees that all threads see the most updated value of the variable but does not provide atomicity, which means simultaneous updates can still cause issues.

class VolatileCounter {
    private volatile int counter = 0;

    public void increment() {
        counter++; // Increment operation is NOT atomic
    }

    public int getCounter() {
        return counter;
    }
}

public class VolatileExample {
    public static void main(String[] args) {
        VolatileCounter vc = new VolatileCounter();

        // Create multiple threads to increment the counter
        Runnable task = () -> {
            for (int i = 0; i < 1000; i++) {
                vc.increment();
            }
        };

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

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

        try {
            thread1.join();
            thread2.join();
            thread3.join();
        } catch (InterruptedException e) {
            System.out.println("Thread interrupted: " + e.getMessage());
        }

        // Print final counter value
        System.out.println("Final Counter Value (volatile): " + vc.getCounter());
    }
}

Issues with above code is The counter++ operation is not atomic, as it involves multiple steps (read, increment, write). Threads may overwrite each other’s updates, leading to incorrect results.

What is local cache

local cache refers to the storage mechanism used by a thread to keep a copy of shared variables or data in its own CPU cache or registers, rather than continually accessing the shared data from main memory. This local cache helps improve performance by allowing threads to access frequently used data more quickly.

Synchronization Counter Example

class SynchronizedCounter {
    private int counter = 0;
    public synchronized void increment() {
        counter++; 
    }
    public synchronized int getCounter() {
        return counter;
    }
}

public class SynchronizedExample {
    public static void main(String[] args) {
        SynchronizedCounter sc = new SynchronizedCounter();

        // Create multiple threads to increment the counter
        Runnable task = () -> {
            for (int i = 0; i < 1000; i++) {
                sc.increment(); 
            }
        };

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

        thread1.start();
        thread2.start();
        thread3.start();
        try {
            thread1.join();
            thread2.join();
            thread3.join();
        } catch (InterruptedException e) {
            System.out.println("Thread interrupted: " + e.getMessage());
        }
        System.out.println("Final Counter Value (synchronized): " + sc.getCounter());
    }
}

Leave a Reply

Your email address will not be published. Required fields are marked *