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.
java
class SharedCounter {
    // Shared data: a simple array and a counter
    private final int[] data;
    private int count = 0;

    public SharedCounter(int capacity) {
        this.data = new int[capacity];
    }

    /**
     * This method is synchronized to ensure only one thread can modify the
     * data and count at a time.
     * @param number The integer to add.
     */
    public synchronized void add(int number) {
        if (count < data.length) {
            System.out.println(Thread.currentThread().getName() + " is adding: " + number);
            data[count] = number;
            // Simulate work to increase the chance of a race condition without sync
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
            count++;
        } else {
            System.out.println("Array is full. Cannot add " + number);
        }
    }

    /**
     * Synchronized method to print the final state of the shared data.
     */
    public synchronized void printFinalState() {
        System.out.println("--- Final State ---");
        System.out.println("Total numbers added: " + count);
        System.out.print("Data: [");
        for (int i = 0; i < count; i++) {
            System.out.print(data[i] + (i == count - 1 ? "" : ", "));
        }
        System.out.println("]");
    }
}

/**
 * A worker task that adds numbers to the shared counter.
 */
class WorkerThread implements Runnable {
    private final SharedCounter sharedCounter;

    public WorkerThread(SharedCounter sharedCounter) {
        this.sharedCounter = sharedCounter;
    }

    @Override
    public void run() {
        // Each thread adds 5 numbers.
        for (int i = 0; i < 5; i++) {
            sharedCounter.add((int) (Math.random() * 100));
        }
    }
}

/**
 * The main class to demonstrate synchronization without using Collections.
 */
public class SynchronizationDemo {

    public static void main(String[] args) {
        System.out.println("Demonstrating synchronization with a shared array.");

        // 1. Create a single instance of the resource with a capacity of 10.
        SharedCounter sharedCounter = new SharedCounter(10);

        // 2. Create two threads that operate on the SAME sharedCounter object.
        Thread thread1 = new Thread(new WorkerThread(sharedCounter), "Worker-1");
        Thread thread2 = new Thread(new WorkerThread(sharedCounter), "Worker-2");

        // 3. Start the threads.
        thread1.start();
        thread2.start();

        // 4. Wait for both threads to finish their work.
        try {
            thread1.join();
            thread2.join();
        } catch (InterruptedException e) {
            System.out.println("Main thread interrupted.");
        }

        // 5. Print the final, consistent state of the shared resource.
        System.out.println("\nAll threads have finished.");
        sharedCounter.printFinalState();
    }
}

Expected Output

The output will show interleaved messages from both threads, but the final result will be consistent and correct, with a total of 10 numbers safely added to the array.

Demonstrating synchronization with a shared array.
Worker-1 is adding: 83
Worker-2 is adding: 45
Worker-1 is adding: 77
Worker-2 is adding: 12
Worker-1 is adding: 91
Worker-2 is adding: 58
Worker-1 is adding: 23
Worker-2 is adding: 33
Worker-1 is adding: 64
Worker-2 is adding: 99

All threads have finished.
--- Final State ---
Total numbers added: 10
Data: [83, 45, 77, 12, 91, 58, 23, 33, 64, 99]

Program Explanation

  1. The Shared Resource (SharedCounter):

    • This class now holds a simple integer array (data) and a count variable that tracks how many numbers have been added. This replaces the ArrayList.
    • The add method is synchronized. This is the most critical part. It ensures that only one thread can be adding a number at any given moment. This prevents two threads from trying to write to the same array index (data[count]) or incrementing count at the same time.
    • It includes a check to make sure the array doesn't overflow.
    • The printFinalState method is also synchronized to ensure it reads the array and count in a consistent state, after all modifications are complete.
  2. The Worker (WorkerThread):

    • This class is a Runnable whose job is to add numbers to the SharedCounter.
    • Each thread instance receives a reference to the single, shared SharedCounter object.
    • In its run method, it calls the synchronized add method multiple times.
  3. The Main Program (SynchronizationDemo):

    • It creates one instance of SharedCounter. This is the resource that needs protection.
    • It creates two threads, passing the same sharedCounter instance to both.
    • It starts both threads. They immediately begin competing to call the add method.
    • join() is used to make the main thread wait until both workers are finished.
    • Finally, it calls printFinalState to show the result. Thanks to synchronization, the final count will be exactly 10 (5 from each thread), and no data will be lost.

Old Implementation

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.