Concurrency Interview question to print even & odd

Concurrency Interview question to print even & odd

Many a times an interview would try to make an assessment of your depth on the subject. For example even if you are able to use some of the advanced constructs of java concurrency and asynchronous task execution , they would still want to understand the evolution of the multi threading an concurrency over the period of time. For example how many ways you can manage and create threads in Java.

However in order to understand and implement the concept of concurrency you may also need to understand how threading works and how can implement the concepts while solving a simple problem.

It is very important to note here that there is no substitute of Synchronization construct when we want multiple threads to talk to share the state of an object. Handling concurrency and creating multiple asynchronous tasks are entirely different concepts and achieves differnt use cases.

Problem:

One of the simples way to achieve that
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class EvenOddPrinter {
    public static void main(String[] args) {
        final int maxNumber = 20; // Define the max number to print to
        ExecutorService executor = Executors.newFixedThreadPool(2); // Create a thread pool with two threads

        Runnable printEven = () -> {
            for (int i = 2; i <= maxNumber; i += 2) { // Start from 2 for even numbers
                System.out.println("Even: " + i);
                try {
                    Thread.sleep(100); // Sleep to simulate some computation and make the output more readable
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt(); // Restore the interrupted status
                    System.out.println("Thread interrupted: " + e.getMessage());
                }
            }
        };

        Runnable printOdd = () -> {
            for (int i = 1; i <= maxNumber; i += 2) { // Start from 1 for odd numbers
                System.out.println("Odd: " + i);
                try {
                    Thread.sleep(100); // Sleep to simulate some computation and make the output more readable
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt(); // Restore the interrupted status
                    System.out.println("Thread interrupted: " + e.getMessage());
                }
            }
        };

        CompletableFuture<Void> evenFuture = CompletableFuture.runAsync(printEven, executor);
        CompletableFuture<Void> oddFuture = CompletableFuture.runAsync(printOdd, executor);

        CompletableFuture<Void> combinedFuture = CompletableFuture.allOf(evenFuture, oddFuture);
        combinedFuture.join(); // Wait for both tasks to complete

        executor.shutdown(); // Shutdown the executor service
    }
}        

Output:

Now Interview is trying to see if you can use threads in their essence without using advanced constructs.

More efficient way without completable Future.

To make the even-odd number printing program more efficient and more synchronized, we can utilize a shared object for coordination between the two threads, ensuring that the even and odd printing tasks take turns. Instead of sleeping, we’ll use wait() and notify() to efficiently manage thread execution. This approach reduces unnecessary waiting and makes the program more responsive.

public class EvenOddPrinterUsingThreads {
    private static final Object lock = new Object();
    private static final int MAX_NUMBER = 20;
    private static int current = 1;

    public static void main(String[] args) {
        Thread evenThread = new Thread(() -> printEvenNumbers());
        Thread oddThread = new Thread(() -> printOddNumbers());

        evenThread.start();
        oddThread.start();
    }

    private static void printEvenNumbers() {
        while (current <= MAX_NUMBER) {
            synchronized (lock) {
                if (current % 2 == 1) { // If current is odd, wait
                    try {
                        lock.wait();
                    } catch (InterruptedException e) {
                        Thread.currentThread().interrupt();
                    }
                } else {
                    System.out.println("Even: " + current);
                    current++;
                    lock.notifyAll(); // Notify the other thread
                }
            }
        }
    }

    private static void printOddNumbers() {
        while (current <= MAX_NUMBER) {
            synchronized (lock) {
                if (current % 2 == 0) { // If current is even, wait
                    try {
                        lock.wait();
                    } catch (InterruptedException e) {
                        Thread.currentThread().interrupt();
                    }
                } else {
                    System.out.println("Odd: " + current);
                    current++;
                    lock.notifyAll(); // Notify the other thread
                }
            }
        }
    }
}        

Output:

Key Improvements:

  • The synchronized block ensures that only one thread can execute its block at a time. This prevents both threads from trying to print numbers simultaneously.
  • Efficient Waiting and Notification: Using wait() and notifyAll() instead of sleep() allows threads to pause execution until it's their turn, reducing idle time.
  • Shared Counter: Both threads share a current counter and use it to determine which numbers to print. This ensures proper sequence without having to calculate or check ranges separately.

How It Works:

  • Both threads start and attempt to print numbers.
  • Depending on the current value, one thread will print its number and increment current, while the other waits.
  • After printing, the thread calls notifyAll() to wake up the other thread and then checks the current value again in its next iteration. If it's not its turn, it will wait again.
  • This process repeats until current exceeds MAX_NUMBER, at which point both threads complete their execution and exit naturally.

This approach is more efficient than the previous example because it eliminates unnecessary sleeping and ensures that the threads work in perfect alternation, thus making the program more responsive and quicker in execution.

Now Interviewer could ask more on this Like.

Its much easier implementation using Predicate interface where in you design for two conditions and pass on those conditions to the completable Future. The only difference is that now the logic of checking odd and even is controlled by the predicate itself.

import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Predicate;

public class EvenOddPrinterWithPredicate {

    private static final int MAX_NUMBER = 20;
    private static AtomicInteger current = new AtomicInteger(1);
    private static final Object lock = new Object();

    public static void main(String[] args) {
        ExecutorService executor = Executors.newFixedThreadPool(2);

        Predicate<Integer> isEven = num -> num % 2 == 0;
        Predicate<Integer> isOdd = isEven.negate();

        CompletableFuture<Void> evenTask = CompletableFuture.runAsync(() -> printNumbers(isEven), executor);
        CompletableFuture<Void> oddTask = CompletableFuture.runAsync(() -> printNumbers(isOdd), executor);

        CompletableFuture.allOf(evenTask, oddTask).join(); // Wait for both tasks to complete

        executor.shutdown();
    }

    private static void printNumbers(Predicate<Integer> condition) {
        while (current.get() <= MAX_NUMBER) {
            synchronized (lock) {
                if (condition.test(current.get())) {
                    System.out.println((condition == isEven ? "Even: " : "Odd: ") + current.getAndIncrement());
                    lock.notify();
                } else {
                    try {
                        lock.wait();
                    } catch (InterruptedException e) {
                        Thread.currentThread().interrupt();
                    }
                }
            }
        }
    }
}        

Key Changes and Features:

  • We define two Predicate<Integer> instances, isEven and isOdd, to encapsulate the conditions for even and odd numbers, respectively. This makes the condition checks within the printNumbers method more expressive and flexible.
  • The printNumbers method takes a Predicate<Integer> as its condition parameter, determining when to print a number. This method is used by both even and odd tasks, reducing code duplication.
  • Functional Programming Style: Using Predicate for condition checks showcases a functional programming style, making the code more modular and easier to understand.
  • CompletableFuture for Asynchronous Execution: The tasks are executed asynchronously using CompletableFuture.runAsync() with an executor, ensuring they run in parallel but are coordinated through synchronization.

In all the examples that we see we have not tried to bench mark the performance however we have tried to solve the same problem in multiple ways in order to exude the depth of the topic.

The key take aways:

  1. Asynchronous event processing does not eliminate the need of synchronization.
  2. Synchronization among threads will always be required when thread are working and communicating with one another.
  3. All of the solutions achieve the same objective but with little bit different programming and construct styles.
  4. This reading does not attempt to prove one listing better over another

要查看或添加评论,请登录

?? Saral Saxena ???????的更多文章

社区洞察

其他会员也浏览了