How can you prepare effectively for your next Java Threads and Concurrency interview?

Top 20 coding questions to nail your next interview

Varsha Das
Level Up Coding

--

Gone are the days when merely knowing about terms like synchronization, race condition, semaphore, deadlock, wait(), notify() would suffice in case of interviews.

Nowadays, if you do not have hands-on practice with threads and concurrency related coding problems, you will find it difficult not just to crack the interview but to also write code for complex real-world scenarios.

Hence the idea is to make your lives a little bit simpler by compiling all the important coding questions around this topic (as much as I could figure out) and presenting them to you guys so that it helps you to revise the questions before an interview.

I have also done something similar with Java 8 Streams, Garbage Collection, Comparators and Hashmap articles, here are the related articles:

Here are some of the topics that we will cover in this article in the form of coding questions, so that by the end of this article you will feel quite confident to tackle 70–80% of the challenges around this topic.

✅ Exploring Race Conditions in Java and How to Prevent Them

✅ Understanding and Handling Java Deadlocks

✅ Implementing Concurrent File Reader Using Executor Framework and Thread Pools

✅ Solving the Producer Consumer Problem in Java with Blocking Queue

✅ Semaphore vs. ReEntrant Lock in Java Concurrency

✅ Comparing CountDownLatch vs CyclicBarrier for Synchronization

✅ Exploring Scheduled Executor Service in Java Concurrency

✅ Java 8 CompletableFuture

and many more….

Also, for further understanding, I have pinpointed the video explanation after every question that you guys can refer to, so that you don’t have to search through the playlists.

Let’s get started.

Develop a Java program where multiple threads access a shared resource, ensuring only one thread accesses it at a time to prevent data inconsistency and race conditions.

Starting with something very basic: synchronization.

Synchronization is used to ensure that only one thread can access the resource at a time.

When a thread invokes a synchronized method, it automatically acquires the intrinsic lock associated with the method’s object, preventing other threads from invoking synchronized methods on the same object concurrently.

By synchronizing the methods, ensure that only one thread can execute the critical section (incrementing or decrementing the count variable) at a time, preventing data races where multiple threads modify the shared resource simultaneously.

//shared resource
private int count = 0;

// Method to increment the count by 1
public synchronized void increment() {
count++;
System.out.println(Thread.currentThread().getName() + " increments count to: " + count);
}

// Method to decrement the count by 1
public synchronized void decrement() {
count--;
System.out.println(Thread.currentThread().getName() + " decrements count to: " + count);
}

// Creating a shared resource
SharedResource sharedResource = new SharedResource();

// Creating and starting multiple threads
for (int i = 0; i < 3; i++) {
new Thread(() -> {
// Accessing the shared resource
sharedResource.increment();
sharedResource.decrement();
}).start();
}

Output:

Write Java code that demonstrates a deadlock situation between two or more threads. Analyze the code and propose a solution to prevent deadlock.

Deadlocks occur when two or more threads are blocked forever, waiting for each other to release resources that they need to continue executing.

public class DeadlockExample {
private static final Object varsha_key = new Object();
private static final Object harsha_key = new Object();

public static void main(String[] args) {
Thread varsha = new Thread(() -> {
synchronized (varsha_key) {
System.out.println("Varsha has acquired her key");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Varsha is waiting for Harsha's key");
synchronized (harsha_key) {
System.out.println("Varsha has acquired Harsha's key");
}
}
});

Thread harsha = new Thread(() -> {
synchronized (harsha_key) {
System.out.println("Harsha has acquired his key");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Harsha is waiting for Varsha's key");
synchronized (varsha_key) {
System.out.println("Harsha has acquired Varsha's key");
}
}
});

varsha.start();
harsha.start();
}
}

The program hangs indefinitely at this point because Varsha is waiting for Harsha's key, and Harsha is waiting for Varsha's key, resulting in a deadlock.

Fixed the code:

public class NoDeadlockExample {
private static final Object varsha_key = new Object();
private static final Object harsha_key = new Object();

public static void main(String[] args) {
Thread varsha = new Thread(() -> {
synchronized (varsha_key) {
System.out.println("Varsha has acquired her key");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Varsha is waiting for Harsha's key");
synchronized (harsha_key) {
System.out.println("Varsha has acquired Harsha's key");
}
}
});

Thread harsha = new Thread(() -> {
synchronized (varsha_key) { // Acquiring in the same order to prevent deadlock
System.out.println("Harsha has acquired his key");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Harsha is waiting for Varsha's key");
synchronized (harsha_key) {
System.out.println("Harsha has acquired Varsha's key");
}
}
});

varsha.start();
harsha.start();
}
}

The program executes without any issues as both threads acquire locks in the same order, preventing deadlock.

Output:

Here is the video explanation on deadlocks with more interview questions:

Create a Java program that exhibits a race condition.

Race conditions occur when multiple threads access shared resources simultaneously, leading to unpredictable behavior or wrong output.

There are three types of race conditions:

1. Read-Modify-Write Race Condition: This occurs when multiple threads read a shared resource, perform some operation, and then write back the modified value. If these operations are not atomic or synchronized, the resulting value can be incorrect due to interleaved execution.

2. Write-Write Race Condition: This occurs when multiple threads write to a shared resource without proper synchronization, leading to conflicting updates and potential data corruption.

3. Check-Then-Act Race Condition: This occurs when the correctness of a program depends on a value remaining unchanged between the time it is checked and the time it is acted upon.

Read-Modify-Write Race Condition:

private static int sharedValue = 0;
public static void readModifyWriteRace() {
for (int i = 0; i < 100000; i++) {
// Read sharedValue
int temp = sharedValue;
// Modify
temp++;
// Write back the updated value to sharedValue
sharedValue = temp;
}
}
public static void main(String[] args) throws InterruptedException {
// Create and start threads
Thread thread1 = new Thread(() -> readModifyWriteRace());
Thread thread2 = new Thread(() -> readModifyWriteRace());
thread1.start();
thread2.start();
// Wait for threads to finish
thread1.join();
thread2.join();
// Print the final value of sharedValue
System.out.println("Final value of sharedValue: " + sharedValue);
}

Output:

Write-Write Race Condition:

Because there’s no coordination between the threads when updating the shared variable counter. As a result, one thread may overwrite the value written by the other thread before it has a chance to finish its operation, leading to unpredictable behavior and potentially incorrect results.

public class WriteWriteRaceCondition {
private static int counter = 0;

public static void main(String[] args) throws InterruptedException {
Thread thread1 = new Thread(() -> {
for (int i = 0; i < 10000; i++) {
counter=1;
}
});

Thread thread2 = new Thread(() -> {
for (int i = 0; i < 10000; i++) {
counter=2;
}
});

thread1.start();
thread2.start();

thread1.join();
thread2.join();

System.out.println("Counter value: " + counter);
}
}

Output:

Check-Then-Act Race Condition:

Let’s consider the scenario of ticket booking system, where each customer thread attempts to book 2 tickets multiple times. This is a classic example of a scenario where Check-Then-Act race conditions can take place.

public class TicketBookingSystem {
private int availableTickets = 5;
public void bookTickets(int numTickets) {
if (numTickets <= availableTickets) { // Check if enough tickets are available
// Simulate a delay in processing the booking
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
availableTickets -= numTickets; // Update the available tickets
System.out.println(Thread.currentThread().getName() + " booked " + numTickets + " tickets.");
} else {
System.out.println(Thread.currentThread().getName() + " couldn't book tickets. Not enough available.");
}
}
public static void main(String[] args) {
TicketBookingSystem bookingSystem = new TicketBookingSystem();
Runnable bookingTask = () -> {
for (int i = 0; i < 3; i++) { // Each customer attempts to book 2 tickets
bookingSystem.bookTickets(2);
}
};
Thread thread1 = new Thread(bookingTask, "Customer 1");
Thread thread2 = new Thread(bookingTask, "Customer 2");
thread1.start();
thread2.start();
}
}

Multiple threads are checking the availability of tickets in a ticket booking system before attempting to book them. If the check for availability (Check) and the booking of tickets (Act) are not performed atomically or without proper synchronization, it can lead to a race condition. One thread might check that there are enough tickets available, but before it can book them, another thread books the tickets, causing the first thread to attempt to book tickets that are no longer available.

Output:

How to solve the race condition problem?

Use Locks: Implement synchronization using locks to control access to shared resources. In Java, you can use synchronized blocks or Lock objects from the java.util.concurrent.locks package. By acquiring a lock before accessing shared data and releasing it afterward, you ensure that only one thread can access the critical section at a time, preventing race conditions.

Atomic Operations: You can use classes like AtomicInteger or AtomicReference from the java.util.concurrent.atomic package. These classes provide atomic versions of common operations like incrementing, decrementing, or setting values, ensuring that modifications to shared data occur without interference from other threads.

Thread-Safe Data Structures: Use thread-safe data structures provided by the language or libraries to handle concurrent access to shared data. For instance, in Java, you can use ConcurrentHashMap, ConcurrentLinkedQueue, or CopyOnWriteArrayList from the java.util.concurrent package. These data structures are designed to handle concurrent access safely, typically by employing internal synchronization mechanisms, thus avoiding race conditions.

Implement a Java program that uses atomic operations from the java.util.concurrent.atomic package to ensure thread safety without explicit synchronization.

Like discussed above to solve race condition, we will use java.concurrent.atomic.AtomicInteger,which is a thread-safe class in Java that provides atomic operations on an integer variable without requiring synchronization.

What happens if we don’t use AtomicInteger:

private static int counter = 0;
public static void main(String[] args) {
Runnable task = () -> {
for (int i = 0; i < 10000; i++) {
counter++; // Non-atomic increment operation
}
};
Thread thread1 = new Thread(task);
Thread thread2 = new Thread(task);
thread1.start();
thread2.start();
try {
thread1.join();
thread2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Counter value: " + counter);
}

Output:

As we can see here, the counter value is incorrect as there is no synchronization present. We can use AtomicInteger to solve this problem, as follows:

private static AtomicInteger atomicCounter = new AtomicInteger(0);
Runnable atomicTask = () -> {
for (int i = 0; i < 10000; i++) {
atomicCounter.incrementAndGet(); // Atomic increment operation
}
};
Thread thread1 = new Thread(atomicTask);
Thread thread2 = new Thread(atomicTask);
thread1.start();
thread2.start();

Output:

Solve the classic producer-consumer problem using threads in Java. Implement a shared buffer and ensure proper synchronization between producer and consumer threads.

The producer-consumer problem revolves around two entities: producers, who create data, and consumers, who process that data.

The challenge lies in ensuring that producers and consumers can work concurrently without interfering with each other, especially when accessing a shared buffer where the data is stored.

Here’s how to implement the solution:

  1. Create a shared buffer where producers can store data and from which consumers can retrieve data. This buffer can be implemented using a data structure like an array, queue, or buffer.
  2. To prevent conflicts when accessing the shared buffer, you must use synchronization mechanisms provided by Java, such as synchronized blocks or locks. These mechanisms ensure that only one thread can access the shared buffer at a time.

We can use wait() and notify() to solve this by having the producer notify the consumer when new items are available in the shared buffer, and having the consumer wait for notifications from the producer before consuming items.

Queue<Integer> buffer = new LinkedList<>();
int maxSize = 5;

Thread producerThread = new Thread(() -> {
for(int i = 0; i< 10; i++){
synchronized (buffer){
//overflow check
while(buffer.size() == maxSize){
try {
System.out.println("buffer is full, so waiting");
buffer.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
buffer.add(i);
System.out.println("Produced "+i);
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
buffer.notifyAll();
}
}
}, "Producer");

Thread consumerThread = new Thread(() -> {
for(int i = 0; i< 10; i++){
synchronized (buffer){
//underflow check
while(buffer.isEmpty()){
try {
System.out.println("buffer is empty, so waiting");
buffer.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
int val = buffer.remove();
System.out.println("Consumed "+val);
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
buffer.notifyAll();
}
}
}, "Consumer");

Output:

Rewrite the producer-consumer problem using the BlockingQueue interface in Java.

BlockingQueue provides a thread-safe queue that handles synchronization and blocking operations, ensuring that producers and consumers can safely interact without explicit wait-notify mechanisms.

BlockingQueue<Integer> buffer = new ArrayBlockingQueue<>(5);
Thread producerThread = new Thread(() -> {
for (int i = 0; i < 10; i++) {
try {
buffer.put(i); // This will block if the buffer is full.
System.out.println("Produced " + i);
Thread.sleep((long) (Math.random() * 5000));
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}, "Producer");

Thread consumerThread = new Thread(() -> {
for (int i = 0; i < 10; i++) {
try {

int value = buffer.take(); // This will block if the buffer is empty.
System.out.println("Consumed " + value);
Thread.sleep(3000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}, "Consumer");

Output:

Write a Java program to demonstrate the usage of ReentrantLock for achieving thread synchronization.

ReentrantLock allows threads to acquire and release locks in a flexible and controlled manner, providing several advantages over synchronized blocks like reentrancy, explicit locking and unlocking using lock() and unlock() methods, etc.

ReentrantLocksupports reentrant locking, meaning a thread that already holds the lock can acquire it again without deadlocking. This feature is beneficial when a method needs to call another method that also acquires the lock.

Threads acquire the lock by calling the lock() method and release it by calling the unlock() method. It's essential to release the lock in a finally block to ensure that the lock gets released even if an exception occurs.

public class ReentrantLockDemo {

public static void main(String[] args) {
ReentrantLock reentrantLock = new ReentrantLock();

reentrantLock.lock(); // counter=1
reentrantLock.lock(); // counter=2

System.out.println(reentrantLock.isLocked());
System.out.println(reentrantLock.isHeldByCurrentThread());

reentrantLock.unlock(); // counter=1
System.out.println(reentrantLock.isLocked());
System.out.println(reentrantLock.getHoldCount());

reentrantLock.lock(); // counter=2

reentrantLock.unlock(); // counter=1
System.out.println(reentrantLock.isLocked());
System.out.println(reentrantLock.getHoldCount());

reentrantLock.unlock(); // counter=0
System.out.println(reentrantLock.isLocked());
System.out.println(reentrantLock.getHoldCount()); // 0
}
}

Output:

Develop a Java program that utilizes the ReadWriteLock interface to manage access to a shared resource where reads are frequent but writes are infrequent.

ReadWriteLock allows multiple threads to acquire the read lock simultaneously, and provides exclusive write access to a single thread, ensuring that only one thread can modify the shared resource at a time.

It allows multiple threads to read the resource concurrently while providing exclusive access for writing. This helps improve performance in scenarios where reads are more frequent than writes.

The main difference between a ReadWriteLock and ReentrantLock lies in their locking strategies -

  1. ReadWriteLock: It consists of two locks, a read lock and a write lock. Multiple threads can acquire the read lock simultaneously if no thread holds the write lock. However, only one thread can hold the write lock, and no other threads can acquire the read or write lock while the write lock is held. This allows for multiple concurrent readers but ensures exclusive access for writers.
  2. ReentrantLock: It’s a general-purpose lock that allows a thread to acquire the lock multiple times. It provides exclusive access to the resource, meaning only one thread can hold the lock at a time.

ReentrantLock doesn’t distinguish between read and write operations; it’s up to the programmer to implement the necessary logic for shared access.

private final ReadWriteLock lock = new ReentrantReadWriteLock();
private int data = 0;
public void write(int newData) {
lock.writeLock().lock();
try {
data = newData;
System.out.println(Thread.currentThread().getName() + " wrote data: " + newData);
} finally {
lock.writeLock().unlock();
}
}

public int read() {
lock.readLock().lock();
try {
System.out.println(Thread.currentThread().getName() + " read data: " + data);
return data;
} finally {
lock.readLock().unlock();
}
}

// Reader threads
for (int i = 1; i <= 3; i++) {
Thread readerThread = new Thread(() -> {
for (int j = 0; j < 3; j++) {
sharedResource.read();
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "Reader-" + i);
readerThread.start();
}

// Writer thread
Thread writerThread = new Thread(() -> {
for (int i = 1; i <= 3; i++) {
sharedResource.write(i);
try {
Thread.sleep(3000); // Simulate infrequent writes
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "Writer");
writerThread.start();

Output:

Demonstrate the usage of semaphores to control access to a shared resource among multiple threads.

Semaphores are integer variables that are used to coordinate access to a shared resource by multiple threads. They help manage the number of threads that can access the resource simultaneously.

Semaphore provides synchronization by allowing a fixed number of threads (permits) to access a shared resource concurrently.
acquire() → If permit is available; acquire lock; decrement counter.
release() Return permit; increment counter.

There are two types of Semaphores:

  1. Binary Semaphores: Permits are either 0 or 1. Either has only one permit available or no permits.
public class BinarySemaphoreDemo {
public static void main(String[] args) {
Semaphore semaphore = new Semaphore(1); // Semaphore with 1 permit
try
{
System.out.println("Thread 1 is trying to acquire a permit.");
semaphore.acquire();
System.out.println("Thread 1 has acquired a permit.");
System.out.println("Available permits: " + semaphore.availablePermits());
Thread.sleep(2000); // Simulate some work
semaphore.release();
System.out.println("Thread 1 has released the permit.");
System.out.println("Available permits: " + semaphore.availablePermits());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}

Output:

2. Counting Semaphores: Permits are greater than 1. Allows a specified number of threads to access a critical section concurrently.

public class CountingSemaphoreExample {
public static void main(String[] args) {
Semaphore countingSemaphore = new Semaphore(3); // Creating a counting semaphore with three permits

// Acquiring semaphore
try {
countingSemaphore.acquire();
System.out.println("Thread 1 has acquired the semaphore.");
System.out.println("Available Permits: " + countingSemaphore.availablePermits()); // Should print 2
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// Releasing semaphore
countingSemaphore.release();
System.out.println("Thread 1 has released the semaphore.");
System.out.println("Available Permits: " + countingSemaphore.availablePermits()); // Should print 3
}

// Acquiring semaphore again
try {
countingSemaphore.acquire();
System.out.println("Thread 2 has acquired the semaphore.");
System.out.println("Available Permits: " + countingSemaphore.availablePermits()); // Should print 2
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// Releasing semaphore again
countingSemaphore.release();
System.out.println("Thread 2 has released the semaphore.");
System.out.println("Available Permits: " + countingSemaphore.availablePermits()); // Should print 3
}
}
}

Output:

Implement a Java program to show how to use the Executor framework and ThreadPoolExecutor to execute multiple tasks concurrently.

Executor Framework simplifies the management of concurrent tasks by decoupling task submission from execution, providing a higher-level abstraction for handling multithreading. It manages a pool of threads, improving performance by reusing threads for executing multiple tasks instead of creating and destroying threads for each task.

ThreadPoolExecutor is an implementation of Executor framework which allows customization of core pool size, maximum pool size, thread keep-alive time, and queue size, enabling fine-tuning of thread pool behavior according to application requirements.

These are various types of ThreadPoolExecutor along with code snippets:

FixedThreadPool:

ExecutorService executor = Executors.newFixedThreadPool(5); // Create a thread pool with 5 threads

CachedThreadPool:

ExecutorService executor = Executors.newCachedThreadPool(); // Create a thread pool with an adjustable number of threads

SingleThreadExecutor:

ExecutorService executor = Executors.newSingleThreadExecutor(); // Create a thread pool with a single thread

ScheduledThreadPool:

ScheduledExecutorService executor = Executors.newScheduledThreadPool(3); // Create a scheduled thread pool with 3 thread
//Create a ThreadPoolExecutor with a fixed number of threads
ExecutorService executor = Executors.newFixedThreadPool(3);

for (int i = 1; i <= 5; i++) {
final int taskNumber = i;
executor.submit(() -> {
System.out.println("Task " + taskNumber + " started");
// Perform some task
try {
Thread.sleep(4000); // Simulate some work
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Task " + taskNumber + " completed");
});
}

// Shutdown the executor once all tasks are submitted
executor.shutdown();

Output:

Write a program that demonstrates the usage of the ScheduledExecutorService to schedule tasks to run periodically at fixed intervals.

The ScheduledExecutorService interface in Java provides a thread pool for scheduling tasks to execute after a delay or at regular intervals. It's part of the java.util.concurrent package and offers more advanced scheduling capabilities compared to the basic Timer class.

//Creating a ScheduledExecutorService with 3 threads
ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(3);

Runnable task = () -> {
System.out.println("Task executed on: "+LocalDateTime.now().toString());
};

//schedule the task to run with an initial delay of 1 second and then every 5 seconds
scheduler.scheduleAtFixedRate(task, 1, 5, TimeUnit.SECONDS);

//keep the program running for a while to observe the periodic execution
try {
Thread.sleep(20 * 1000); //run for 20 seconds
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}

//shutdown the scheduler when done
scheduler.shutdown();

Output:

Implement a Java program where multiple Callable tasks are submitted to a thread pool using ExecutorService. Retrieve the results using Future objects and handle exceptions if any.

Callable interface represents a task that can be executed asynchronously and returns a result. It’s similar to the Runnable interface but can return a result or throw an exception.

Future interface represents the result of an asynchronous computation and provides methods to check if the computation is complete, retrieve the result, and cancel the task if necessary.

When combined, Callable and Future allow for asynchronous execution of tasks and retrieval of their results, enabling more flexibility and efficiency.

Callable tasks can be submitted to ExecutorService implementations, such as ThreadPoolExecutor or ScheduledThreadPoolExecutor, for execution, and the resulting Future objects can be used to retrieve the task’s result or handle exceptions that may occur during execution.

Let’s see an example where we will fetch stock prices for a list of symbols asynchronously and print the results, using Callable and Future.

List<String> symbols = List.of("ABC", "PQR", "TFGF", "YEDS", "PFS");
List<Future> futures = new ArrayList<>();
ExecutorService executorService = Executors.newFixedThreadPool(1);

for (String symbol : symbols) {
Callable<Double> stockSymbolTask = new StockPriceFetcher(symbol);
System.out.println("submitting for "+symbol);
Future<Double> future = executorService.submit(stockSymbolTask);
System.out.println("Future = "+future);
futures.add(future);
}
executorService.shutdown();

for(int i = 0 ;i<5;i++){
try {
System.out.println("Stock from " + symbols.get(i) + " price = " + futures.get(i).get() + " future status " + futures.get(i));
} catch(InterruptedException | ExecutionException e){
System.out.println(e);
}
}

Output:

Create a CustomThreadFactory implementation in Java to customize the creation of threads within a thread pool.

CustomThreadFactory is a class that implements ThreadFactory interface and allows the creation of custom threads with specific characteristics like naming conventions, priority levels, daemon status, and exception handling.

public class CustomThreadFactoryDemo {
public static void main(String[] args) {
// Creating a custom thread factory

ThreadFactory threadFactory = new CustomThreadFactory();
// Creating a ThreadPoolExecutor with custom configuration
ThreadPoolExecutor executor = new ThreadPoolExecutor(
3, //core pool size
5, //maximum pool size
30, //keep-alive time
TimeUnit.SECONDS, //time unit for keep-alive time
new LinkedBlockingQueue<>(), //task queue
threadFactory //thread factory
);


//Submitting tasks to the thread pool
for (int i = 1; i <= 10; i++) {
final int taskId = i;
executor.submit(() -> {
System.out.println("Task " + taskId + " executed by thread: " + Thread.currentThread().getName());
});
}
executor.shutdown(); //Shutting down the thread pool
}


// Custom thread factory to create threads with a specific naming convention
static class CustomThreadFactory implements ThreadFactory {
private static int threadCount = 1;


@Override
public Thread newThread(Runnable r) {
Thread thread = new Thread(r, "MyCustomThread-" + threadCount++);
return thread;
}
}
}

Output:

Write a program to demonstrate the usage of CountdownLatch.

CountDownLatch allows one or more threads to wait until a set of operations, represented by a count, completes before proceeding.

Threads call the await() method to wait until the count reaches zero.

As other threads complete their tasks, they call the countDown() method to decrement the count. When the count reaches zero, the waiting threads are released, and they can proceed with their execution.

Let’s see an example where we perform multiple validation tasks in a data processing system, where each task is represented by a separate thread, with the help of CountDownLatch.

private final CountDownLatch latch;
public DataProcessingSystem(int validationTaskCount) {
this.latch = new CountDownLatch(validationTaskCount);

}
public void startValidation() {
// Create and start three validation tasks
Thread validator1 = new Thread(new Validator("Validator 1"));
Thread validator2 = new Thread(new Validator("Validator 2"));
Thread validator3 = new Thread(new Validator("Validator 3"));
validator1.start();
validator2.start();
validator3.start();
try {
// Main thread waits until latch count reaches 0
latch.await();
System.out.println("All validation tasks have completed. Data validation completed.");
} catch (InterruptedException e) {
e.printStackTrace();
}
}

public void run() {
System.out.println(name + " is performing validation...");
try {
// Simulating validation by sleeping for a random duration
Thread.sleep((long) (Math.random() * 5000));
System.out.println(name + " has completed validation.");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// Decrements the latch count
latch.countDown();
}
}

Output:

Write a program to demonstrate the usage of CyclicBarrier.

CyclicBarrier allows a group of threads to wait for each other to reach a common barrier point before proceeding.
Each thread can call the
await() method to signal that it has reached the barrier point. When the specified number of threads have called await(), the barrier is tripped, and all threads are released to continue their execution.

To understand how CyclicBarrier works, let us think of a hiking scenario where multiple hikers are hiking together and must reach a checkpoint before continuing. We can simulate this scenario with the help of CyclicBarrier, as shown below:

private static final int NUM_HIKERS = 4;
private static final CyclicBarrier checkpoint = new CyclicBarrier(NUM_HIKERS);
for (int i = 1; i <= NUM_HIKERS; i++) {
Hiker hiker = new Hiker("Hiker " + i);
hiker.start();
}
public void run() {
System.out.println(getName() + " started hiking.");
try {
// Simulate hiking to a checkpoint
Thread.sleep((long) (Math.random() * 5000));

System.out.println(getName() + " reached a checkpoint.");
// Wait for other hikers to reach the checkpoint
checkpoint.await();

// All hikers have reached the checkpoint, continue hiking
System.out.println(getName() + " continues hiking.");

} catch (Exception e) {
e.printStackTrace();
}
}

Output:

For more information, I recommend checking this out:

Write a Java program to demonstrate the usage of CompletableFuture for asynchronous and concurrent programming, such as chaining tasks and combining results.

CompletableFuture allows tasks to be executed asynchronously, meaning that they can run concurrently with other tasks without blocking the calling thread.

public class CompletableFutureDemo {
public static void main(String[] args) {
// supplyAsync
CompletableFuture<Integer> future1 = CompletableFuture.supplyAsync(() -> {
// Simulate a long-running computation
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return 10;
});

// Use the result of the CompletableFuture
future1.thenAccept(result -> System.out.println("Result: " + result));

CompletableFuture<Void> future = CompletableFuture.runAsync(() -> {
//Simulate a long-running task
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Task completed");
});

future.thenRun(() -> System.out.println("Callback after task completion"));


// thenApply
CompletableFuture<Integer> future2 = future1.thenApply(result -> 20 * result);

System.out.println("Future 2: " +future2.join());


// thenCompose
CompletableFuture<Integer> future3 = CompletableFuture.supplyAsync(() -> 20)
.thenCompose(result -> CompletableFuture.supplyAsync(() -> result * 2));



System.out.println("Future 3 "+future3.join());



// thenCombine
CompletableFuture<Integer> future4 = CompletableFuture.supplyAsync(() -> 10);
CompletableFuture<Integer> future5 = CompletableFuture.supplyAsync(() -> 20);
CompletableFuture<Integer> combinedFuture = future4.thenCombine(future5, (result1, result2) -> result1 + result2);



CompletableFuture.allOf(future1, future, future2, future3, combinedFuture).join();

System.out.println("Combined Future -"+combinedFuture.join());


// exceptionally
CompletableFuture<Object> future6 = CompletableFuture.supplyAsync(() -> {
throw new RuntimeException("Exception occurred");
}).exceptionally(exception -> {
System.out.println("Exception occurred: " + exception.getMessage());
return 0;
});
// allOf
CompletableFuture<Integer> future7 = CompletableFuture.supplyAsync(() -> 10);
CompletableFuture<Integer> future8 = CompletableFuture.supplyAsync(() -> 20);
CompletableFuture<Void> allFutures = CompletableFuture.allOf(future7, future8);

// thenRun
allFutures.thenRun(() -> System.out.println("All futures completed"));

// Join all futures to wait for their completion
CompletableFuture.allOf(future1, future2, future3, future4, future5, future6, future7, future8).join();
}
}

Output:

Covered a 2-part video series with more LIVE code demo on CompletableFuture, here is the video:

Well, congratulations if you have made it to the end of this article.

Thanks for reading.

Here are the playlist links:

If you liked this article, please click the “clap” button 👏 a few times.

It gives me enough motivation to put out more content like this. Please share it with a friend who you think this article might help.

Connect with me — Varsha Das | LinkedIn

If you’re seeking personalized guidance in software engineering, career development, core Java, Systems design, or interview preparation, let’s connect here.

Rest assured, I’m not just committed; I pour my heart and soul into every interaction. I have a genuine passion for decoding complex problems, offering customised solutions, and connecting with individuals from diverse backgrounds.

Follow my Youtube channel — Code With Ease — By Varsha, where we discuss Java & Data Structures & Algorithms and so much more.

Subscribe here to receive alerts whenever I publish an article.

Happy learning and growing together.

--

--

"Senior Software Engineer @Fintech | Digital Creator @Youtube | Thrive on daily excellence | ❤️ complexity -> clarity | Devoted to health and continuous growth