Thread Synchronization in Java

 Thread Synchronization in Java

Thread synchronization in Java is a mechanism to control the access of multiple threads to shared resources. Without synchronization, it’s possible for one thread to modify a shared variable while another thread is in the middle of using or updating the same variable, leading to inconsistent results.

Why Synchronization?

Consider a scenario where multiple threads are trying to access and modify a shared resource like a bank account balance. Without synchronization, two threads could simultaneously read the balance, add a different amount, and write back the new balance, leading to a lost update.

Achieving Synchronization

Java provides several mechanisms to achieve thread synchronization:

  • Synchronized Method
  • Synchronized Block
  • Reentrant Locks (in java.util.concurrent.locks package)

Synchronized Method

When you declare a method as synchronized, the thread holds the monitor for that method’s object. If another thread is executing the synchronized method, any other thread that tries to call any synchronized method on the same object will be blocked until the first thread is done.

Example:
class Counter {
    private int count = 0;

    // Synchronized method to ensure thread-safe increment
    public synchronized void increment() {
        count++;
    }

    public int getCount() {
        return count;
    }
}
class CounterIncrementer extends Thread {
    private final Counter counter;

    public CounterIncrementer(Counter counter) {
        this.counter = counter;
    }

    @Override
    public void run() {
        for (int i = 0; i < 1000; i++) {
            counter.increment();
        }
    }
}
public class Main {
    public static void main(String[] args) {
        Counter counter = new Counter();

        CounterIncrementer t1 = new CounterIncrementer(counter);
        CounterIncrementer t2 = new CounterIncrementer(counter);

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

        try {
            t1.join();
            t2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

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

Explanation
  1. Counter Class:

    • Contains the shared resource count.
    • The increment method is declared as synchronized to ensure that only one thread can execute this method at a time, making it thread-safe.
  2. CounterIncrementer Class:

    • Extends the Thread class and is responsible for incrementing the counter.
    • In the run method, it loops to increment the counter 1000 times.
    • The CounterIncrementer constructor takes a Counter object as a parameter to ensure the shared resource is accessed.
  3. Main Class:

    • Creates a shared Counter object.
    • Creates two CounterIncrementer threads, each initialized with the shared Counter instance.
    • Starts both threads and waits for them to complete using join.
    • Prints the final count.

This implementation uses the Thread class directly, demonstrating thread creation and execution by extending Thread. The synchronized method ensures that the increment method is executed atomically by one thread at a time.

Synchronized Block

A synchronized block is used to synchronize a specific block of code inside a method. It is more efficient than synchronized methods because you can control the scope of synchronization, limiting it to a critical section.

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

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

    public int getCount() {
        return count;
    }
}
class CounterIncrementer extends Thread {
    private final Counter counter;

    public CounterIncrementer(Counter counter) {
        this.counter = counter;
    }

    @Override
    public void run() {
        for (int i = 0; i < 1000; i++) {
            counter.increment();
        }
    }
}
public class Main {
    public static void main(String[] args) {
        Counter counter = new Counter();

        CounterIncrementer t1 = new CounterIncrementer(counter);
        CounterIncrementer t2 = new CounterIncrementer(counter);

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

        try {
            t1.join();
            t2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

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

Explanation
  1. Counter Class:

    • Contains the shared resource count.
    • The increment method uses a synchronized block to ensure thread-safe access to the count variable. The lock object is used as a monitor to synchronize the block.
  2. CounterIncrementer Class:

    • Extends the Thread class and is responsible for incrementing the counter.
    • In the run method, it loops to increment the counter 1000 times.
    • The CounterIncrementer constructor takes a Counter object as a parameter to ensure the shared resource is accessed.
  3. Main Class:

    • Creates a shared Counter object.
    • Creates two CounterIncrementer threads, each initialized with the shared Counter instance.
    • Starts both threads and waits for them to complete using join.
    • Prints the final count.

This implementation uses a synchronized block within the Counter class to control access to the count variable, ensuring that only one thread can increment the counter at a time. The lock object is used to synchronize the block of code that modifies the shared resource.

Reentrant Locks

Reentrant locks provide more flexibility and features compared to synchronized methods/blocks. They are part of the java.util.concurrent.locks package and offer advanced operations like try-lock and timed-lock.

Below is the example implemented using ReentrantLock from the java.util.concurrent.locks package instead of synchronized blocks or methods.

import java.util.concurrent.locks.ReentrantLock;

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

    public void increment() {
        lock.lock();
        try {
            count++;
        } finally {
            lock.unlock();
        }
    }

    public int getCount() {
        return count;
    }
}
class CounterIncrementer extends Thread {
    private final Counter counter;

    public CounterIncrementer(Counter counter) {
        this.counter = counter;
    }

    @Override
    public void run() {
        for (int i = 0; i < 1000; i++) {
            counter.increment();
        }
    }
}
public class Main {
    public static void main(String[] args) {
        Counter counter = new Counter();

        CounterIncrementer t1 = new CounterIncrementer(counter);
        CounterIncrementer t2 = new CounterIncrementer(counter);

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

        try {
            t1.join();
            t2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

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

Explanation
  1. Counter Class:

    • Contains the shared resource count.
    • Uses a ReentrantLock object to ensure thread-safe access to the count variable.
    • The increment method locks the lock before updating count and unlocks it in a finally block to ensure that the lock is released even if an exception is thrown.
  2. CounterIncrementer Class:

    • Extends the Thread class and is responsible for incrementing the counter.
    • In the run method, it loops to increment the counter 1000 times.
    • The CounterIncrementer constructor takes a Counter object as a parameter to ensure the shared resource is accessed.
  3. Main Class:

    • Creates a shared Counter object.
    • Creates two CounterIncrementer threads, each initialized with the shared Counter instance.
    • Starts both threads and waits for them to complete using join.
    • Prints the final count.

This implementation uses ReentrantLock to control access to the shared count variable. The lock provides more flexibility and control compared to synchronized methods or blocks, allowing for more complex synchronization scenarios. The lock and unlock methods are used to acquire and release the lock, respectively. The use of a finally block ensures that the lock is always released, even if an exception occurs.

Comments

Popular posts from this blog

KTU OOP LAB JAVA CSL 203 BTech CS S3 - Dr Binu V P

String Problems

Reading and Writing Text Files