CPU vs IO Bound Sample Java Implementation (4-Core Optimized)

CPU/IO Bound Java (4-Core Optimized)

Here’s the code, optimized for a 4-core . The following sections provide a detailed explanation of the code and the concepts behind it.


import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.RecursiveTask;

public class CPUBoundMultiThreaded {

    static class CalculationTask extends RecursiveTask<Long> {
        private final long start;  // Start of the range to calculate
        private final long end;    // End of the range to calculate

        public CalculationTask(long start, long end) {
            this.start = start;
            this.end = end;
        }

        @Override
        protected Long compute() {
            // Base case: If the range is small enough, perform the calculation directly
            if (end - start <= 10000) {
                long result = 0;
                for (long i = start; i < end; i++) {
                    result += Math.sqrt(i); // Intensive computation
                }
                return result;
            } else {
                // Recursive case: Divide the range into two halves
                long mid = (start + end) / 2;
                CalculationTask leftTask = new CalculationTask(start, mid);  // Create task for the left half
                CalculationTask rightTask = new CalculationTask(mid, end); // Create task for the right half
                leftTask.fork();         // Asynchronously start the left task in another thread
                long rightResult = rightTask.compute(); // Calculate the right half in the current thread
                long leftResult = leftTask.join();    // Wait for the left task to finish and get its result
                return leftResult + rightResult;       // Combine the results
            }
        }
    }

    public static void main(String[] args) {
        long startTime = System.currentTimeMillis();
        long result = 0;

        // Optimal thread pool size for a 4-core CPU for CPU-bound tasks
        int numThreads = 4;
        ForkJoinPool pool = new ForkJoinPool(numThreads); // Create ForkJoinPool with 4 threads
        result = pool.invoke(new CalculationTask(0, 1000000000)); // Start the main task

        long endTime = System.currentTimeMillis();
        System.out.println("Result: " + result);
        System.out.println("Time taken: " + (endTime - startTime) + "ms");
        pool.shutdown(); // Important: Shut down the pool to release resources
    }
}
                    

Explanation of CPU-bound Code:

  • CalculationTask Class:
    • This class extends RecursiveTask, which is part of the Fork/Join framework, designed for recursive, divide-and-conquer tasks.
    • The compute() method is where the actual computation happens.
    • Base Case: If the range of numbers to process (end – start) is small enough (<= 10000 in this case), the task performs the calculation directly. This is important to avoid creating too many small tasks, which can add overhead.
    • Recursive Case: If the range is large, the task divides it into two halves, creates two new CalculationTask objects for each half, and recursively calls compute() on them. The leftTask.fork() schedules the left half to be executed asynchronously, potentially by another thread in the pool. The current thread calculates the right half (rightTask.compute()). Then, leftTask.join() waits for the left task to complete and retrieves the result. Finally, the results from the left and right halves are combined.
  • main Method:
    • A ForkJoinPool is created with a fixed number of threads (4 in this case, to match the number of CPU cores). The Fork/Join framework uses a work-stealing to efficiently distribute tasks among the threads in the pool.
    • The initial CalculationTask (representing the entire problem) is submitted to the ForkJoinPool using pool.invoke(). This starts the computation.
    • The pool.shutdown() method is called to release the resources used by the thread pool after the computation is complete. This is crucial for proper resource management.
  • for 4-Core CPU:
    • The ForkJoinPool is initialized with 4 threads. For CPU-bound tasks, setting the number of threads in the pool to be equal to the number of CPU cores generally provides the best . This ensures that all CPU cores are fully utilized, while minimizing the overhead of context switching between threads.

I/O-bound Processing (4-Core Optimized)


import java.io.*;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.Future;

public class IOBoundMultiThreaded {

    static class FileReadTask implements Callable<Long> {
        private final String fileName;    // The name of the file to read
        private final long startLine;   // The starting line number to read
        private final long endLine;     // The ending line number to read

        public FileReadTask(String fileName, long startLine, long endLine) {
            this.fileName = fileName;
            this.startLine = startLine;
            this.endLine = endLine;
        }

        @Override
        public Long call() throws Exception {
            long lineCount = 0;
            try (BufferedReader br = new BufferedReader(new FileReader(fileName))) {
                String line;
                long currentLine = 0;
                while ((line = br.readLine()) != null && currentLine < endLine) {
                    if (currentLine >= startLine) {
                        lineCount++; // Increment the line count if it's within the desired range
                    }
                    currentLine++;
                }
            }
            return lineCount; // Return the number of lines read
        }
    }

    public static void main(String[] args) throws Exception {
        long startTime = System.currentTimeMillis();
        final String fileName = "largefile.txt"; // The name of the large file

        // More threads for I/O-bound, but let's be reasonable for a 4-core CPU
        int numThreads = 8; // Increased, but not excessively
        ExecutorService executorService = Executors.newFixedThreadPool(numThreads); // Create a thread pool
        List<Future<Long>> futures = new ArrayList<>(); // List to store the results of the tasks

        long totalLines = 100000; // Total number of lines in the file (replace with actual number)
        int linesPerThread = (int) Math.ceil((double) totalLines / numThreads); // Calculate lines per thread

        // Submit tasks to the thread pool
        for (int i = 0; i < numThreads; i++) {
            long startLine = i * linesPerThread;
            long endLine = Math.min(totalLines, (i + 1) * linesPerThread); // Ensure endLine doesn't exceed totalLines
            futures.add(executorService.submit(new FileReadTask(fileName, startLine, endLine))); // Submit task
        }

        long totalLineCount = 0;
        // Collect the results from the tasks
        for (Future<Long> future : futures) {
            totalLineCount += future.get(); // Get the result (number of lines read) from each task
        }

        executorService.shutdown();             // Shutdown the executor service
        executorService.awaitTermination(10, TimeUnit.SECONDS); // Wait for tasks to complete

        long endTime = System.currentTimeMillis();
        System.out.println("Total lines: " + totalLineCount);
        System.out.println("Time taken: " + (endTime - startTime) + "ms");
    }
}
                    

Explanation of I/O-bound Code:

  • FileReadTask Class:
    • This class implements Callable, which represents a task that returns a result.
    • The call() method reads a portion of the file (from startLine to endLine) and counts the number of lines in that portion.
    • It uses a BufferedReader for efficient reading of the file. The try-with-resources statement ensures that the BufferedReader is automatically closed.
  • main Method:
    • An ExecutorService (a thread pool) is created using Executors.newFixedThreadPool(numThreads). For I/O-bound tasks, it’s often beneficial to have more threads than CPU cores.
    • The code calculates how many lines each thread should read (linesPerThread).
    • A loop submits a FileReadTask to the ExecutorService for each portion of the file. The executorService.submit() method returns a Future, which represents the result of the asynchronous computation. These Future objects are stored in a list.
    • After all tasks are submitted, the code iterates through the list of Future objects, calling future.get() to retrieve the number of lines read by each task. The results are summed to get the total line count. The future.get() method blocks until the task has completed.
    • The executorService.shutdown() method is called to signal that no more tasks will be submitted to the pool. The executorService.awaitTermination() method waits for the tasks to complete (up to a specified timeout). This is important for proper cleanup.
  • Optimization for 4-Core CPU:
    • The ExecutorService is initialized with 8 threads. For I/O-bound tasks, a common strategy is to use more threads than available CPU cores. This is because I/O operations involve waiting (e.g., waiting for data from disk). While one thread is waiting for I/O, other threads can proceed with other I/O operations, thus improving overall throughput. However, setting the number of threads too high can also lead to performance problems (e.g., excessive context switching, resource contention), so it’s a balance. For a 4-core CPU, 8 is often a reasonable starting point for I/O-bound tasks, but the optimal value can vary.

Key Concepts

  • CPU-bound: The process is limited by the speed of the CPU. More CPU power = faster processing.
  • I/O-bound: The process is limited by the speed of I/O operations. Faster I/O or more efficient I/O handling = faster processing.
  • Multi-threading: A technique that allows multiple threads of execution to run concurrently within a single program. This can improve performance by utilizing multiple CPU cores or by overlapping I/O operations with computation.
  • Fork/Join Framework: A framework in Java for parallelizing divide-and-conquer tasks. It uses a work-stealing algorithm for efficient thread management. Best suited for CPU-bound tasks that can be broken down into smaller subtasks.
  • ExecutorService: A framework in Java for managing thread pools. It provides methods for submitting tasks to a pool of threads and managing their lifecycle. Useful for both CPU-bound and I/O-bound tasks.
  • Callable and Future: Callable represents a task that returns a result and may throw an exception. Future represents the result of an asynchronous computation.

This detailed explanation should give you a better understanding of how to optimize Java code for CPU-bound and I/O-bound scenarios on a 4-core CPU. Let me know if you have any further questions.

Agentic AI (9) AI (178) AI Agent (21) airflow (4) Algorithm (36) Algorithms (31) apache (41) API (108) Automation (11) Autonomous (26) auto scaling (3) AWS (30) Azure (22) BigQuery (18) bigtable (3) Career (7) Chatbot (21) cloud (87) cosmosdb (1) cpu (24) database (82) Databricks (13) Data structure (17) Design (76) dynamodb (4) ELK (1) embeddings (14) emr (4) flink (10) gcp (16) Generative AI (8) gpu (11) graphql (4) image (6) index (10) indexing (12) interview (6) java (39) json (54) Kafka (19) Life (43) LLM (25) LLMs (10) Mcp (2) monitoring (55) Monolith (6) N8n (12) Networking (14) NLU (2) node.js (9) Nodejs (6) nosql (14) Optimization (38) performance (54) Platform (87) Platforms (57) postgres (17) productivity (7) programming (17) pseudo code (1) python (55) RAG (132) rasa (3) rdbms (2) ReactJS (2) realtime (1) redis (6) Restful (6) rust (6) Spark (27) sql (43) time series (6) tips (1) tricks (13) Trie (62) vector (22) Vertex AI (11) Workflow (52)

Leave a Reply

Your email address will not be published. Required fields are marked *