Readers-Writers Problem in Java Threads Using Conditional Synchronization

Conditional synchronization is a technique used to control the access to shared resources based on certain conditions. This approach is often used in scenarios like the Readers-Writers problem, where the conditions for accessing shared data vary depending on whether the accessing thread is a reader or a writer.

In the context of the Readers-Writers problem, a conditional synchronization solution typically involves using condition variables alongside locks to manage access to the shared resource. Condition variables allow threads to wait for certain conditions to be met before proceeding.

ReadersWritersControl.java
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
 
public class ReadersWritersControl {
    private int readersCount = 0;
    private boolean isWriting = false;
 
    private final Lock lock = new ReentrantLock();
    private final Condition canRead = lock.newCondition();
    private final Condition canWrite = lock.newCondition();
 
    public void startRead() throws InterruptedException {
        lock.lock();
        try {
            while (isWriting) {
                canRead.await();
            }
            readersCount++;
        } finally {
            lock.unlock();
        }
    }
 
    public void endRead() {
        lock.lock();
        try {
            readersCount--;
            if (readersCount == 0) {
                canWrite.signal();
            }
        } finally {
            lock.unlock();
        }
    }
 
    public void startWrite() throws InterruptedException {
        lock.lock();
        try {
            while (isWriting || readersCount > 0) {
                canWrite.await();
            }
            isWriting = true;
        } finally {
            lock.unlock();
        }
    }
 
    public void endWrite() {
        lock.lock();
        try {
            isWriting = false;
            canRead.signalAll();
            canWrite.signal();
        } finally {
            lock.unlock();
        }
    }
 
    static class Reader implements Runnable {
        private final ReadersWritersControl control;
 
        Reader(ReadersWritersControl control) {
            this.control = control;
        }
 
        @Override
        public void run() {
            try {
                control.startRead();
                System.out.println(Thread.currentThread().getName() + " is reading");
                Thread.sleep(1000); // Simulating reading operation
                control.endRead();
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        }
    }
 
    static class Writer implements Runnable {
        private final ReadersWritersControl control;
 
        Writer(ReadersWritersControl control) {
            this.control = control;
        }
 
        @Override
        public void run() {
            try {
                control.startWrite();
                System.out.println(Thread.currentThread().getName() + " is writing");
                Thread.sleep(1000); // Simulating writing operation
                control.endWrite();
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        }
    }
 
    public static void main(String[] args) {
        ReadersWritersControl control = new ReadersWritersControl();
        Thread reader1 = new Thread(new Reader(control), "Reader 1");
        Thread reader2 = new Thread(new Reader(control), "Reader 2");
        Thread writer1 = new Thread(new Writer(control), "Writer 1");
        Thread writer2 = new Thread(new Writer(control), "Writer 2");
 
        writer1.start();
        reader1.start();
        reader2.start();
        writer2.start();
    }
}

ReadersWritersControl Class: This class manages the reader and writer access to a shared resource. It uses a ReentrantLock to enforce mutual exclusion and two Condition objects (canRead and canWrite) to control when readers and writers are allowed to access the resource.

Reader and Writer Classes: These inner classes implement the Runnable interface and define the behaviors for readers and writers. They use the methods provided by the ReadersWritersControl class to safely start and end their reading or writing operations.

Reading and Writing Methods: Methods like startRead, endRead, startWrite, and endWrite in the ReadersWritersControl class manage the access to the shared resource. Readers can read concurrently unless a writer is actively writing, and writers obtain exclusive access to ensure data integrity.

Condition Variables Usage: The canRead and canWrite condition variables are used to manage which threads (readers or writers) should wait and which can access the shared resource. The conditions are based on the current state variables readersCount and isWriting. For instance, if a writer is currently writing (isWriting is true), then readers must wait (canRead.await()). Similarly, writers must wait (canWrite.await()) if another writer is already writing or if there are any readers currently reading.

Concurrency Control with Locks: The use of the ReentrantLock ensures that changes to the state variables (readersCount and isWriting) are made in a thread-safe manner. This lock provides mutual exclusion, ensuring that only one thread can modify the state at any given time.

Efficient Synchronization: The condition variables (canRead and canWrite) efficiently manage the waiting and notification mechanism for both readers and writers. This allows threads to sleep and be woken up only when necessary, instead of continuously checking the resource's state (busy-waiting).

Balancing Read and Write Operations: The solution aims to balance the needs of both readers and writers. It allows multiple readers to access the resource concurrently (enhancing read throughput) while ensuring writers have exclusive access when writing (ensuring data consistency).

Practical Application: This implementation is particularly useful in scenarios where a resource is frequently read but less frequently written to. The ability for multiple readers to access the resource concurrently without blocking each other (as long as no writer is active) can significantly improve performance in read-heavy applications.

This Readers-Writers problem implementation with conditional synchronization in Java is a classic example of coordinating access to shared resources in a multi-threaded environment. It effectively addresses the need for both concurrency among readers and exclusive access for writers, ensuring that the system can handle multiple access patterns efficiently and safely.