Multithreading

  1. Real-life example: For a large data set

Multithreading can be used to perform data processing tasks in parallel:

The BankDataProcessing class loads the data from a database or file, splits the data into multiple lists for processing in parallel, creates a thread pool with a fixed number of threads, and submits each data chunk for processing in a separate thread. Once all tasks are completed, the thread pool is shutdown.

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

class DataProcessor implements Runnable {
    private List<Data> dataList;
    private String filter;

    public DataProcessor(List<Data> dataList, String filter) {
        this.dataList = dataList;
        this.filter = filter;
    }

    public void run() {
        // Perform data processing task, such as filtering or aggregation
        List<Data> filteredData = new ArrayList<>();
        for (Data data : dataList) {
            if (data.getCategory().equals(filter)) {
                filteredData.add(data);
            }
        }
        // Do something with the filtered data, such as updating a database or generating a report
    }
}

public class BankDataProcessing {
    public static void main(String[] args) {
        // Load data from database or file
        List<Data> allData = loadData();

        // Split the data into multiple lists for processing in parallel
        List<List<Data>> dataChunks = splitData(allData, 10);

        // Create a thread pool with a fixed number of threads
        int numThreads = 4;
        ExecutorService executor = Executors.newFixedThreadPool(numThreads);

        // Submit each data chunk for processing in a separate thread
        String filter = "Transaction";
        for (List<Data> dataChunk : dataChunks) {
            Runnable processor = new DataProcessor(dataChunk, filter);
            executor.submit(processor);
        }
        // Shutdown the thread pool once all tasks are completed
        executor.shutdown();
    }

    private static List<Data> loadData() {
        // Load data from database
    }

    private static List<List<Data>> splitData(List<Data> allData, int chunkSize) {
        List<List<Data>> dataChunks = new ArrayList<>();
        int numChunks = (int) Math.ceil((double) allData.size() / chunkSize);
        for (int i = 0; i < numChunks; i++) {
            int start = i * chunkSize;
            int end = Math.min(start + chunkSize, allData.size());
            List<Data> chunk = allData.subList(start, end);
            dataChunks.add(chunk);
        }
        return dataChunks;
    }
}
  1. wait() & notify()

    Object.wait()

    The wait() method causes current thread to wait until another thread invokes the notify() method or the notifyAll() method for this object, or a specified amount of time has elapsed.

    Object.notify():

    The notify() method wakes up a single thread that is waiting on this object. If any threads are waiting on this object, one of them is chosen to be awakened.

public class WaitAndNotify {

    public static void main(String[] args) throws InterruptedException {
        final Object lock = new Object();
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (lock) {
                    System.out.println("Thread 1 is waiting...");
                    try {
                        lock.wait();  // t1 remain in wait state until some other thread invoke notify on the same object
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                    System.out.println("Thread 1 has been notified");
                }
            }
        });

        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (lock) {
                    System.out.println("Thread 2 is running");
                    lock.notify();   // notifies t1 to wake up
                }
            }
        });

        t1.start();
        Thread.sleep(1000);
        t2.start();
    }
}
  1. join()

    When the main thread invokes the join() method then, it is said that the main thread is in the waiting state. The main thread then waits for the child threads to complete their tasks. When the child threads complete their job, a notification is sent to the main thread, which again moves the thread from waiting to the active state.

    OR

    When the join() method is invoked, the current thread stops its execution and the thread goes into the wait state. The current thread remains in the wait state until the thread on which the join() method is invoked has achieved its dead state. If interruption of the thread occurs, then it throws the InterruptedException.

public class MainThreadExample {
    public static void main(String[] args) {
        Thread t1 = new Thread(new ChildThread());
        Thread t2 = new Thread(new ChildThread());

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

        try {
            t1.join(); // main thread waits for t1 to complete
            t2.join(); // main thread waits for t2 to complete
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println("All child threads have completed their tasks, main thread resumes.");
    }
}

class ChildThread implements Runnable {
    @Override
    public void run() {
        System.out.println("Child thread started.");
        try {
            Thread.sleep(5000); // simulate some task being performed by the child thread
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("Child thread completed.");
    }
}