Skip to content

Question 5

Modify the above program (4) to

  • include synchronization in a scenario where multiple threads are updating a shared list of integers.
  • Implement a ThreadSafeList class with synchronized methods to manage concurrent access.
  • Write a Java program that demonstrates how synchronization prevents data inconsistencies and race conditions when multiple threads add and remove elements from the list.

1. ThreadSafeList.java (The class with synchronized methods)

java
import java.util.ArrayList;
import java.util.List;

public class ThreadSafeList {
    private final List<Integer> list = new ArrayList<>();

    // Synchronized method to add an element
    public synchronized void add(Integer element) {
        list.add(element);
        System.out.println(Thread.currentThread().getName() + " added: " + element +
                           ". Current list: " + list.toString());
        // Simulate some processing time
        try {
            Thread.sleep(10);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt(); // Restore interrupted status
            System.err.println(Thread.currentThread().getName() + " was interrupted during add.");
        }
    }

    // Synchronized method to remove an element (first occurrence)
    public synchronized boolean removeElement(Integer element) {
        boolean removed = list.remove(element);
        if (removed) {
            System.out.println(Thread.currentThread().getName() + " removed: " + element +
                               ". Current list: " + list.toString());
        } else {
            System.out.println(Thread.currentThread().getName() + " tried to remove: " + element +
                               " (not found). Current list: " + list.toString());
        }
        try {
            Thread.sleep(15); // Simulate some processing time
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            System.err.println(Thread.currentThread().getName() + " was interrupted during remove.");
        }
        return removed;
    }

    // Synchronized method to remove element at a specific index
    public synchronized Integer removeAtIndex(int index) {
        if (index >= 0 && index < list.size()) {
            Integer removedElement = list.remove(index);
            System.out.println(Thread.currentThread().getName() + " removed at index " + index + ": " + removedElement +
                               ". Current list: " + list.toString());
            try {
                Thread.sleep(15);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
                System.err.println(Thread.currentThread().getName() + " was interrupted during removeAtIndex.");
            }
            return removedElement;
        } else {
            System.out.println(Thread.currentThread().getName() + " tried to remove at index " + index +
                               " (out of bounds). Current list: " + list.toString());
            return null;
        }
    }


    // Synchronized method to get the list size
    public synchronized int size() {
        // System.out.println(Thread.currentThread().getName() + " getting size: " + list.size());
        return list.size();
    }

    // Synchronized method to print the list (prevents ConcurrentModificationException)
    public synchronized void printList() {
        System.out.println(Thread.currentThread().getName() + " printing list: " + new ArrayList<>(list)); // Print a copy
    }

    // For comparison: what happens without synchronization (DON'T USE THIS IN PRODUCTION FOR SHARED LISTS)
    public void addUnsafe(Integer element) {
        list.add(element); // Potential race condition
        System.out.println(Thread.currentThread().getName() + " (UNSAFE) added: " + element +
                           ". Current list size: " + list.size());
    }

    public boolean removeElementUnsafe(Integer element) {
        boolean removed = list.remove(element); // Potential race condition
        if (removed) {
            System.out.println(Thread.currentThread().getName() + " (UNSAFE) removed: " + element +
                               ". Current list size: " + list.size());
        }
        return removed;
    }
}

Explanation of ThreadSafeList:

  • private final List<Integer> list = new ArrayList<>();: We encapsulate a standard ArrayList. It's final because the reference to the list object itself won't change, though its contents will.
  • synchronized keyword:
    • When a method is declared synchronized, it means that only one thread can execute any synchronized method on a particular instance of ThreadSafeList at any given time.
    • Java achieves this using an intrinsic lock (also called a monitor lock) associated with each object.
    • When a thread calls a synchronized method, it attempts to acquire the lock for that object.
    • If the lock is available, the thread acquires it, executes the method, and then releases the lock.
    • If the lock is already held by another thread, the current thread will block (wait) until the lock is released.
  • add(), removeElement(), removeAtIndex(), size(), printList(): All these methods access or modify the shared list. Making them synchronized ensures that these operations are atomic with respect to each other for a given ThreadSafeList instance. This prevents:
    • Race Conditions: Where the outcome of the computation depends on the unpredictable timing of threads (e.g., two threads trying to add to the list simultaneously might corrupt its internal state if ArrayList.add itself isn't internally fully thread-safe for all combined operations).
    • Data Inconsistencies: Such as one thread reading the size while another is halfway through adding an element.
    • ConcurrentModificationException: If one thread tries to iterate or print the list while another thread is modifying it (e.g., adding or removing elements), this exception can occur with non-thread-safe collections. Synchronizing printList (or iterating within a synchronized block) prevents this.
  • addUnsafe() and removeElementUnsafe() are provided for conceptual comparison to show what not to do.

2. ListManipulator.java (Runnable task to modify the list)

java
import java.util.Random;

public class ListManipulator implements Runnable {
    private final ThreadSafeList safeList;
    private final boolean isAdder; // True if this task adds, false if it removes
    private final int operations;
    private final Random random = new Random();

    public ListManipulator(ThreadSafeList safeList, boolean isAdder, int operations) {
        this.safeList = safeList;
        this.isAdder = isAdder;
        this.operations = operations;
    }

    @Override
    public void run() {
        for (int i = 0; i < operations; i++) {
            if (isAdder) {
                int valueToAdd = random.nextInt(100); // Add random numbers 0-99
                safeList.add(valueToAdd);
            } else {
                // Try to remove from the start of the list if not empty
                if (safeList.size() > 0) {
                    safeList.removeAtIndex(0);
                } else {
                    // System.out.println(Thread.currentThread().getName() + " found list empty, cannot remove.");
                    try {
                        Thread.sleep(5); // Wait a bit if list is empty before trying again
                    } catch (InterruptedException e) {
                        Thread.currentThread().interrupt();
                        break; // Exit loop if interrupted
                    }
                }
            }

            try {
                // Random sleep to make interleaving more apparent and less predictable
                Thread.sleep(random.nextInt(20) + 5);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt(); // Preserve interrupt status
                System.err.println(Thread.currentThread().getName() + " was interrupted in main loop.");
                break; // Exit loop if interrupted
            }
        }
        System.out.println(Thread.currentThread().getName() + " finished its operations.");
    }
}

3. SynchronizedDemo.java (Main application)

java
public class SynchronizedDemo {
    public static void main(String[] args) {
        System.out.println("--- Main Thread Started ---");

        ThreadSafeList sharedList = new ThreadSafeList();
        int numOperationsPerThread = 10; // Each thread will perform this many add/remove operations

        // Create Adder Threads
        Thread adder1 = new Thread(new ListManipulator(sharedList, true, numOperationsPerThread), "Adder-1");
        Thread adder2 = new Thread(new ListManipulator(sharedList, true, numOperationsPerThread), "Adder-2");
        Thread adder3 = new Thread(new ListManipulator(sharedList, true, numOperationsPerThread), "Adder-3");


        // Create Remover Threads
        Thread remover1 = new Thread(new ListManipulator(sharedList, false, numOperationsPerThread), "Remover-1");
        Thread remover2 = new Thread(new ListManipulator(sharedList, false, numOperationsPerThread), "Remover-2");

        System.out.println("\nStarting threads to manipulate the synchronized list...");

        // Start all threads
        adder1.start();
        remover1.start(); // Mix start order
        adder2.start();
        remover2.start();
        adder3.start();


        // Wait for all threads to complete their work
        try {
            adder1.join();
            adder2.join();
            adder3.join();
            remover1.join();
            remover2.join();
            System.out.println("\nAll manipulator threads have completed.");
        } catch (InterruptedException e) {
            System.err.println("Main thread interrupted while waiting: " + e.getMessage());
            Thread.currentThread().interrupt(); // Preserve interrupt status
        }

        System.out.println("\n--- Final State of the List ---");
        sharedList.printList();
        System.out.println("Final list size reported by main thread: " + sharedList.size());

        // For comparison - uncomment to see potential issues (may or may not throw an exception quickly)
        /*
        System.out.println("\n\n--- DEMONSTRATING UNSAFE ACCESS (EXPECT POTENTIAL ISSUES) ---");
        ThreadSafeList unsafeSharedList = new ThreadSafeList(); // Using the same class, but calling unsafe methods
        numOperationsPerThread = 500; // Increase operations to make issues more likely

        Thread unsafeAdder1 = new Thread(() -> {
            for (int i = 0; i < numOperationsPerThread; i++) unsafeSharedList.addUnsafe(i);
        }, "UnsafeAdder-1");
        Thread unsafeAdder2 = new Thread(() -> {
            for (int i = 0; i < numOperationsPerThread; i++) unsafeSharedList.addUnsafe(i + 1000);
        }, "UnsafeAdder-2");
        Thread unsafeRemover1 = new Thread(() -> {
            for (int i = 0; i < numOperationsPerThread / 2; i++) unsafeSharedList.removeElementUnsafe(i);
        }, "UnsafeRemover-1");


        unsafeAdder1.start();
        unsafeAdder2.start();
        unsafeRemover1.start(); // May cause ConcurrentModificationException if other threads iterate


        try {
            unsafeAdder1.join();
            unsafeAdder2.join();
            unsafeRemover1.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("Final UNSAFE list size: " + unsafeSharedList.list.size()); // Accessing internal list directly (bad practice)
        // You might see inconsistent sizes or ConcurrentModificationException if you try to iterate it here
        // while other threads are still (hypothetically) running or if the internal state is corrupt.
        */

        System.out.println("\n--- Main Thread Exiting ---");
    }
}

How to Compile and Run:

  1. Save the files: ThreadSafeList.java, ListManipulator.java, SynchronizedDemo.java.
  2. Compile:
    bash
    javac ThreadSafeList.java ListManipulator.java SynchronizedDemo.java
  3. Run:
    bash
    java SynchronizedDemo

Output and Observations:

  • You will see interleaved output from "Adder" and "Remover" threads.
  • Each line of output for an add or remove operation will show the thread name, the action, the element involved, and the state of the list after that operation.
  • Because the methods in ThreadSafeList are synchronized, you will not see partial updates or corrupted list states in the printouts from within those synchronized methods. For example, you won't see a size reported that doesn't match the elements listed if both size() and the list iteration for printing are synchronized.
  • The final list size should be (3 * numOperationsPerThread) - (number of successful removals). Since removals only happen if the list is not empty, the number of successful removals might be less than 2 * numOperationsPerThread if removers run too fast initially.
  • Crucially, you should NOT get a ConcurrentModificationException when the list is printed or its size is checked, because those operations are also synchronized, ensuring exclusive access.

What if Synchronization Was Not Used?

If you were to use a plain ArrayList directly from multiple threads without any synchronization (or call the addUnsafe/removeElementUnsafe methods in the example), you might encounter:

  1. ConcurrentModificationException: If one thread tries to iterate the list (e.g., for printing or in list.toString()) while another thread is adding or removing elements.
  2. Incorrect Size: list.size() might return an inconsistent value if a thread is in the middle of an add/remove operation.
  3. Lost Updates/Corrupted Data: The internal structure of the ArrayList could become corrupted (e.g., its internal array holding elements and its size counter get out of sync), leading to ArrayIndexOutOfBoundsException or incorrect elements being retrieved.
  4. Race Conditions: The final state of the list could be unpredictable and vary from run to run.

The synchronized keyword provides a simple and effective way to ensure that only one thread can modify or access the critical sections (methods operating on the shared list) at a time, thus preventing these concurrency issues.

Made with ❤️ for students, by a fellow learner.