0% found this document useful (0 votes)
3 views

Top Java Multithreading Q&A

The document provides 10 coding examples demonstrating the use of CompletableFuture and ExecutorService in Java, along with explanations for each example. It also discusses concurrency issues such as race conditions and deadlocks, and presents two approaches for implementing a thread-safe counter. Additionally, it includes solutions to common multithreading problems like the Producer-Consumer problem and an Even-Odd number printer.

Uploaded by

umanagarjuna
Copyright
© © All Rights Reserved
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
3 views

Top Java Multithreading Q&A

The document provides 10 coding examples demonstrating the use of CompletableFuture and ExecutorService in Java, along with explanations for each example. It also discusses concurrency issues such as race conditions and deadlocks, and presents two approaches for implementing a thread-safe counter. Additionally, it includes solutions to common multithreading problems like the Producer-Consumer problem and an Even-Odd number printer.

Uploaded by

umanagarjuna
Copyright
© © All Rights Reserved
Available Formats
Download as PDF, TXT or read online on Scribd
You are on page 1/ 16

Below are 10 top coding examples that demonstrate various use cases of CompletableFuture and ExecutorService

in Java. Each example is accompanied by a brief explanation and a complete code snippet.

1. Basic ExecutorService with Fixed Thread Pool

Creates a fixed-size thread pool, submits a task, and shuts down the pool.

java

import java.util.concurrent.ExecutorService;​
import java.util.concurrent.Executors;​

public class ExecutorServiceExample {​
public static void main(String[] args) {​
ExecutorService executor = Executors.newFixedThreadPool(3);​
executor.submit(() -> {​
System.out.println("Hello from ExecutorService");​
});​
executor.shutdown();​
}​
}

Explanation:​
A fixed thread pool of 3 threads is created. A simple task is submitted, and then the executor is gracefully shut down.

2. Submitting Tasks and Retrieving Results Using Future

Submits a Callable task that returns a value and waits for the result.

java
import java.util.concurrent.Callable;​
import java.util.concurrent.ExecutionException;​
import java.util.concurrent.ExecutorService;​
import java.util.concurrent.Executors;​
import java.util.concurrent.Future;​

public class FutureExample {​
public static void main(String[] args) {​
ExecutorService executor = Executors.newFixedThreadPool(2);​
Callable<Integer> task = () -> {​
Thread.sleep(1000);​
return 123;​
};​
Future<Integer> future = executor.submit(task);​
try {​
Integer result = future.get();​
System.out.println("Result: " + result);​
} catch (InterruptedException | ExecutionException e) {​
e.printStackTrace();​
}​
executor.shutdown();​

1
}​
}

Explanation:​
This example demonstrates using a Callable to return a value. The Future is used to wait for and retrieve the
computed result.

3. Properly Shutting Down ExecutorService

Shows how to gracefully shut down an ExecutorService and wait for tasks to finish.

java
import java.util.concurrent.ExecutorService;​
import java.util.concurrent.Executors;​
import java.util.concurrent.TimeUnit;​

public class ShutdownExample {​
public static void main(String[] args) {​
ExecutorService executor = Executors.newFixedThreadPool(2);​
executor.submit(() -> System.out.println("Task running"));​
executor.shutdown();​
try {​
if (!executor.awaitTermination(5, TimeUnit.SECONDS)) {​
executor.shutdownNow();​
}​
} catch (InterruptedException e) {​
executor.shutdownNow();​
}​
}​
}

Explanation:​
After submitting tasks, the code waits for termination. If tasks don't complete within 5 seconds, it forces shutdown.

4. Scheduling Delayed Tasks with ScheduledExecutorService

Uses a ScheduledExecutorService to execute a task after a delay.

java
import java.util.concurrent.Executors;​
import java.util.concurrent.ScheduledExecutorService;​
import java.util.concurrent.TimeUnit;​

public class ScheduledExecutorExample {​
public static void main(String[] args) {​
ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);​
scheduler.schedule(() -> {​
System.out.println("Delayed task executed");​
}, 3, TimeUnit.SECONDS);​
scheduler.shutdown();​

2
}​
}

Explanation:​
A scheduled task is executed after a 3-second delay using a single-threaded scheduler.

5. Canceling a Long-Running Task Using ExecutorService

Submits a long-running task and cancels it after a specified period.

java
import java.util.concurrent.*;​

public class CancelTaskExample {​
public static void main(String[] args) throws InterruptedException {​
ExecutorService executor = Executors.newFixedThreadPool(1);​
Future<?> future = executor.submit(() -> {​
try {​
while (true) {​
System.out.println("Running task...");​
Thread.sleep(500);​
}​
} catch (InterruptedException e) {​
System.out.println("Task interrupted");​
}​
});​

Thread.sleep(2000);​
System.out.println("Cancelling task...");​
future.cancel(true);​
executor.shutdown();​
}​
}

Explanation:​
A continuous task is started and, after 2 seconds, it is canceled by calling future.cancel(true), interrupting the
running thread.

6. Basic CompletableFuture Usage with supplyAsync

Demonstrates how to run a task asynchronously and retrieve the result.

java
import java.util.concurrent.CompletableFuture;​
import java.util.concurrent.ExecutionException;​

public class CompletableFutureExample {​
public static void main(String[] args) {​
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> "Hello from
CompletableFuture");​

3
try {​
String result = future.get();​
System.out.println(result);​
} catch (InterruptedException | ExecutionException e) {​
e.printStackTrace();​
}​
}​
}

Explanation:​
A task is executed asynchronously with supplyAsync, returning a string that is retrieved using get().

7. Chaining CompletableFutures with thenApply and thenCompose

Chains operations to transform a result and perform further asynchronous computation.

java
import java.util.concurrent.CompletableFuture;​
import java.util.concurrent.ExecutionException;​

public class CompletableFutureChainExample {​
public static void main(String[] args) throws ExecutionException, InterruptedException {​
CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> 5)​
.thenApply(n -> n * 2) // Multiply by 2​
.thenCompose(result -> CompletableFuture.supplyAsync(() -> result + 10)); //
Add 10 asynchronously​

System.out.println("Result: " + future.get()); // Expected: (5 * 2) + 10 = 20​
}​
}

Explanation:​
The first stage supplies a number, the second stage doubles it, and the third stage adds 10 asynchronously using
thenCompose.

8. Combining Two CompletableFutures Using thenCombine

Combines the results of two independent tasks.

java
import java.util.concurrent.CompletableFuture;​
import java.util.concurrent.ExecutionException;​

public class CompletableFutureCombineExample {​
public static void main(String[] args) throws ExecutionException, InterruptedException {​
CompletableFuture<Integer> future1 = CompletableFuture.supplyAsync(() -> 20);​
CompletableFuture<Integer> future2 = CompletableFuture.supplyAsync(() -> 22);​
CompletableFuture<Integer> combinedFuture = future1.thenCombine(future2,
Integer::sum);​
System.out.println("Combined result: " + combinedFuture.get()); // Expected: 42​

4
}​
}

Explanation:​
Two asynchronous tasks are run independently, and their results are combined using thenCombine with a sum
operation.

9. Aggregating Multiple CompletableFutures with allOf

Waits for multiple CompletableFutures to complete before proceeding.

java
import java.util.concurrent.CompletableFuture;​

public class CompletableFutureAllOfExample {​
public static void main(String[] args) {​
CompletableFuture<String> future1 = CompletableFuture.supplyAsync(() -> "Task1");​
CompletableFuture<String> future2 = CompletableFuture.supplyAsync(() -> "Task2");​
CompletableFuture<String> future3 = CompletableFuture.supplyAsync(() -> "Task3");​

CompletableFuture<Void> allFutures = CompletableFuture.allOf(future1, future2,
future3);​
allFutures.join(); // Wait until all tasks complete​

try {​
System.out.println("Future1: " + future1.get());​
System.out.println("Future2: " + future2.get());​
System.out.println("Future3: " + future3.get());​
} catch (Exception e) {​
e.printStackTrace();​
}​
}​
}

Explanation:​
CompletableFuture.allOf is used to combine multiple futures. The program waits until all futures are complete
before retrieving individual results.

10. Handling Exceptions in CompletableFuture with exceptionally

Demonstrates error handling by providing a fallback value when an exception occurs.

java
import java.util.concurrent.CompletableFuture;​

public class CompletableFutureExceptionExample {​
public static void main(String[] args) {​
CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> {​
if (true) {​
throw new RuntimeException("Something went wrong!");​

5
}​
return 10;​
}).exceptionally(ex -> {​
System.out.println("Exception: " + ex.getMessage());​
return 0;​
});​

System.out.println("Result: " + future.join()); // Expected output: 0​
}​
}

Explanation:​
If the asynchronous task fails, the exceptionally block catches the exception and returns a fallback value (0).

These 10 examples cover a broad range of common patterns and techniques using ExecutorService and
CompletableFuture. They provide a solid foundation for building robust concurrent applications in Java.

3. Concurrency & Multithreading


Q5: Describe a race condition and a deadlock in multithreaded Java applications. How can they be avoided?​
A5:

●​ Race Condition: Occurs when multiple threads access shared data concurrently and at least one thread
modifies it, leading to unpredictable results.​

○​ Prevention: Use proper synchronization (e.g., synchronized, locks, or atomic variables).​

●​ Deadlock: Happens when two or more threads are waiting for each other to release locks, causing the
threads to be blocked indefinitely.​

○​ Prevention:​

■​ Acquire locks in a consistent global order.​

■​ Use timeout features in lock attempts.​

■​ Minimize the scope of synchronized blocks.​

Q6: Implement a thread-safe counter using two approaches. Explain the trade-offs.

Approach 1: Using Synchronized Methods

java

public class SynchronizedCounter {​


private int count = 0;​

public synchronized void increment() {​
count++;​

6
}​

public synchronized int getCount() {​
return count;​
}​
}

Explanation:​
This approach ensures that only one thread can execute either method at a time. It’s simple and effective, but may
suffer from performance bottlenecks when many threads contend for the same lock.

Approach 2: Using AtomicInteger

java

import java.util.concurrent.atomic.AtomicInteger;​

public class AtomicCounter {​
private AtomicInteger count = new AtomicInteger(0);​

public void increment() {​
count.incrementAndGet();​
}​

public int getCount() {​
return count.get();​
}​
}

Explanation:​
Atomic variables use low-level CPU instructions to perform atomic operations, reducing the overhead of locks. This
method scales better under high contention but is limited to specific atomic operations.

Coding Exercise Recap: Thread-Safe Counter


1. Using Synchronized Methods
This implementation uses the object's intrinsic lock to ensure that only one thread can execute the critical sections at
a time.

java

public class SynchronizedCounter {​


private int count = 0;​

// Increments the counter in a thread-safe manner​
public synchronized void increment() {​
count++;​
}​

7
// Returns the current count in a thread-safe manner​
public synchronized int getCount() {​
return count;​
}​

public static void main(String[] args) throws InterruptedException {​
SynchronizedCounter counter = new SynchronizedCounter();​

// Create multiple threads to increment the counter​
Thread t1 = new Thread(() -> {​
for (int i = 0; i < 1000; i++) {​
counter.increment();​
}​
});​
Thread t2 = new Thread(() -> {​
for (int i = 0; i < 1000; i++) {​
counter.increment();​
}​
});​

t1.start();​
t2.start();​
t1.join();​
t2.join();​

System.out.println("Synchronized Counter Value: " + counter.getCount());​
}​
}

2. Using AtomicInteger
This version leverages low-level CPU instructions for atomicity, which minimizes the overhead of explicit
synchronization.

java

import java.util.concurrent.atomic.AtomicInteger;​

public class AtomicCounter {​
private AtomicInteger count = new AtomicInteger(0);​

// Increments the counter atomically​
public void increment() {​
count.incrementAndGet();​
}​

// Retrieves the current value atomically​
public int getCount() {​
return count.get();​
}​

public static void main(String[] args) throws InterruptedException {​
AtomicCounter counter = new AtomicCounter();​

Thread t1 = new Thread(() -> {​
for (int i = 0; i < 1000; i++) {​

8
counter.increment();​
}​
});​
Thread t2 = new Thread(() -> {​
for (int i = 0; i < 1000; i++) {​
counter.increment();​
}​
});​

t1.start();​
t2.start();​
t1.join();​
t2.join();​

System.out.println("Atomic Counter Value: " + counter.getCount());​
}​
}

Top 3 Multithreading Coding Questions and Solutions


Question 1: Producer-Consumer Problem (Bounded Buffer)
Problem:​
Implement a bounded buffer using the wait() and notifyAll() methods where producers add items to the buffer
and consumers remove items.

Solution:

java

import java.util.LinkedList;​
import java.util.Queue;​

public class BoundedBuffer<T> {​
private final Queue<T> queue = new LinkedList<>();​
private final int capacity;​

public BoundedBuffer(int capacity) {​
this.capacity = capacity;​
}​

// Producer method: waits if the buffer is full​
public synchronized void put(T item) throws InterruptedException {​
while (queue.size() == capacity) {​
wait();​
}​
queue.add(item);​
notifyAll(); // Notify consumers that an item is available​
}​

// Consumer method: waits if the buffer is empty​
public synchronized T take() throws InterruptedException {​

9
while (queue.isEmpty()) {​
wait();​
}​
T item = queue.poll();​
notifyAll(); // Notify producers that space is available​
return item;​
}​

public static void main(String[] args) {​
BoundedBuffer<Integer> buffer = new BoundedBuffer<>(5);​

// Producer thread​
Thread producer = new Thread(() -> {​
for (int i = 1; i <= 10; i++) {​
try {​
buffer.put(i);​
System.out.println("Produced: " + i);​
} catch (InterruptedException e) {​
Thread.currentThread().interrupt();​
}​
}​
});​

// Consumer thread​
Thread consumer = new Thread(() -> {​
for (int i = 1; i <= 10; i++) {​
try {​
int value = buffer.take();​
System.out.println("Consumed: " + value);​
} catch (InterruptedException e) {​
Thread.currentThread().interrupt();​
}​
}​
});​

producer.start();​
consumer.start();​
}​
}

Question 2: Even-Odd Number Printer


Problem:​
Using two threads, print numbers from 1 to N such that one thread prints even numbers and the other prints odd
numbers in order.

Solution:

java

public class EvenOddPrinter {​


private int count = 1;​
private final int limit;​

10
public EvenOddPrinter(int limit) {​
this.limit = limit;​
}​

// Method for printing odd numbers​
public synchronized void printOdd() {​
while (count <= limit) {​
while (count % 2 == 0) {​
try {​
wait();​
} catch (InterruptedException e) {​
Thread.currentThread().interrupt();​
}​
}​
if (count <= limit) {​
System.out.println("Odd: " + count);​
count++;​
notifyAll();​
}​
}​
}​

// Method for printing even numbers​
public synchronized void printEven() {​
while (count <= limit) {​
while (count % 2 != 0) {​
try {​
wait();​
} catch (InterruptedException e) {​
Thread.currentThread().interrupt();​
}​
}​
if (count <= limit) {​
System.out.println("Even: " + count);​
count++;​
notifyAll();​
}​
}​
}​

public static void main(String[] args) {​
int limit = 10;​
EvenOddPrinter printer = new EvenOddPrinter(limit);​

Thread oddThread = new Thread(printer::printOdd);​
Thread evenThread = new Thread(printer::printEven);​

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

Question 3: Singleton Pattern Using Double-Checked Locking


Problem:​
Implement a thread-safe Singleton class using the double-checked locking pattern to ensure lazy initialization with
minimal synchronization overhead.

11
Solution:

java

public class Singleton {​


// Volatile ensures that multiple threads handle the uniqueInstance variable correctly​
private static volatile Singleton instance;​

// Private constructor prevents instantiation from other classes​
private Singleton() {}​

// Double-checked locking for lazy initialization​
public static Singleton getInstance() {​
if (instance == null) {​
synchronized(Singleton.class) {​
if (instance == null) {​
instance = new Singleton();​
}​
}​
}​
return instance;​
}​

public void showMessage() {​
System.out.println("Hello from Singleton!");​
}​

public static void main(String[] args) {​
Singleton singleton = Singleton.getInstance();​
singleton.showMessage();​
}​
}

These examples and exercises cover a range of multithreading topics from synchronization and atomic operations to
classic concurrency problems. Use these examples to practice your coding skills and deepen your understanding of
Java concurrency concepts.

You said:

3 more top multithreaded

ChatGPT said:

Reasoned for 27 seconds

Below are three additional multithreading challenges with complete solutions, each addressing a distinct aspect of
concurrent programming in Java.

Question 4: Dining Philosophers Problem

12
Problem:​
Simulate the Dining Philosophers problem while avoiding deadlock. Philosophers alternate between thinking and
eating, but each must acquire two forks (shared resources) to eat. Use a strategy (for example, alternating fork
acquisition order) to prevent deadlock.

Solution:

java

import java.util.concurrent.locks.ReentrantLock;​

class Philosopher extends Thread {​
private final int id;​
private final ReentrantLock leftFork;​
private final ReentrantLock rightFork;​

public Philosopher(int id, ReentrantLock leftFork, ReentrantLock rightFork) {​
this.id = id;​
this.leftFork = leftFork;​
this.rightFork = rightFork;​
}​

@Override​
public void run() {​
try {​
while (true) {​
think();​
// Even-numbered philosophers pick left fork first; odd-numbered pick right
fork first​
if (id % 2 == 0) {​
leftFork.lock();​
rightFork.lock();​
} else {​
rightFork.lock();​
leftFork.lock();​
}​

eat();​
// Release locks​
leftFork.unlock();​
rightFork.unlock();​
}​
} catch (InterruptedException e) {​
Thread.currentThread().interrupt();​
}​
}​

private void think() throws InterruptedException {​
System.out.println("Philosopher " + id + " is thinking.");​
Thread.sleep((int)(Math.random() * 100));​
}​

private void eat() throws InterruptedException {​
System.out.println("Philosopher " + id + " is eating.");​
Thread.sleep((int)(Math.random() * 100));​
}​
}​

public class DiningPhilosophers {​
public static void main(String[] args) {​

13
int numPhilosophers = 5;​
ReentrantLock[] forks = new ReentrantLock[numPhilosophers];​

for (int i = 0; i < numPhilosophers; i++) {​
forks[i] = new ReentrantLock();​
}​

Philosopher[] philosophers = new Philosopher[numPhilosophers];​
for (int i = 0; i < numPhilosophers; i++) {​
ReentrantLock leftFork = forks[i];​
ReentrantLock rightFork = forks[(i + 1) % numPhilosophers];​
philosophers[i] = new Philosopher(i, leftFork, rightFork);​
philosophers[i].start();​
}​
}​
}

Explanation:​
This solution uses a simple strategy: even and odd philosophers acquire forks in a different order, preventing circular
waiting and thus avoiding deadlock.

Question 5: Implementing a Simple Thread Pool Executor


Problem:​
Implement a basic thread pool that executes submitted tasks using a fixed number of worker threads. Use a blocking
queue to manage the tasks.

Solution:

java

import java.util.concurrent.BlockingQueue;​
import java.util.concurrent.LinkedBlockingQueue;​

public class SimpleThreadPool {​
private final BlockingQueue<Runnable> taskQueue;​
private final WorkerThread[] workers;​
private volatile boolean isStopped = false;​

public SimpleThreadPool(int numThreads) {​
taskQueue = new LinkedBlockingQueue<>();​
workers = new WorkerThread[numThreads];​

for (int i = 0; i < numThreads; i++) {​
workers[i] = new WorkerThread(taskQueue);​
workers[i].start();​
}​
}​

public void execute(Runnable task) {​
if (isStopped) {​
throw new IllegalStateException("ThreadPool is stopped");​
}​

14
taskQueue.offer(task);​
}​

public void shutdown() {​
isStopped = true;​
for (WorkerThread worker : workers) {​
worker.interrupt();​
}​
}​

private static class WorkerThread extends Thread {​
private final BlockingQueue<Runnable> taskQueue;​

public WorkerThread(BlockingQueue<Runnable> queue) {​
this.taskQueue = queue;​
}​

@Override​
public void run() {​
try {​
while (!Thread.currentThread().isInterrupted()) {​
Runnable task = taskQueue.take();​
task.run();​
}​
} catch (InterruptedException e) {​
// Thread is interrupted; exit gracefully​
Thread.currentThread().interrupt();​
}​
}​
}​

public static void main(String[] args) throws InterruptedException {​
SimpleThreadPool threadPool = new SimpleThreadPool(3);​

for (int i = 1; i <= 10; i++) {​
int taskNumber = i;​
threadPool.execute(() -> {​
System.out.println("Executing task " + taskNumber + " by " +
Thread.currentThread().getName());​
try {​
Thread.sleep(500);​
} catch (InterruptedException e) {​
Thread.currentThread().interrupt();​
}​
});​
}​

// Allow tasks to complete before shutting down​
Thread.sleep(5000);​
threadPool.shutdown();​
}​
}

Explanation:​
This thread pool creates a fixed number of worker threads that continuously take tasks from a
LinkedBlockingQueue. The pool supports graceful shutdown by interrupting the workers.

15
Question 6: Barrier Synchronization Using CyclicBarrier
Problem:​
Use a CyclicBarrier to synchronize a group of threads. All threads should perform a task and then wait at the
barrier until all have reached it before continuing.

Solution:

java

import java.util.concurrent.BrokenBarrierException;​
import java.util.concurrent.CyclicBarrier;​

public class BarrierExample {​
public static void main(String[] args) {​
int parties = 5;​
CyclicBarrier barrier = new CyclicBarrier(parties, ​
() -> System.out.println("All threads reached the barrier. Continuing
execution..."));​

for (int i = 0; i < parties; i++) {​
new Thread(new Task(barrier), "Thread-" + i).start();​
}​
}​
}​

class Task implements Runnable {​
private final CyclicBarrier barrier;​

public Task(CyclicBarrier barrier) {​
this.barrier = barrier;​
}​

@Override​
public void run() {​
try {​
System.out.println(Thread.currentThread().getName() + " is performing part 1 of
the task.");​
Thread.sleep((int) (Math.random() * 1000));​
System.out.println(Thread.currentThread().getName() + " is waiting at the
barrier.");​
barrier.await(); // Wait until all threads reach this point​

// After barrier is tripped, continue with further tasks​
System.out.println(Thread.currentThread().getName() + " resumed execution after
barrier.");​
} catch (InterruptedException | BrokenBarrierException e) {​
e.printStackTrace();​
}​
}​
}

Explanation:​
In this example, five threads perform some work and then call barrier.await(). Only when all threads have
reached the barrier does the barrier action execute (printing a message), and all threads continue their execution.

16

You might also like