Java Concurrency Tools & Modern Java Techniques Interview Questions

Last Updated : 25 Aug, 2025

This focuses on advanced interview questions around Java's concurrency utilities and modern parallelism techniques. It covers the Executor framework, thread pools, Callable, Future, and CompletableFuture for asynchronous programming. You’ll also explore synchronization tools like ReentrantLock, CountDownLatch, Semaphore, Phaser, and atomic classes, essential for writing scalable, non-blocking concurrent applications.

1. What is the difference between execute() and submit() in the Executor framework? In what scenarios would you prefer one over the other?

In Java, the Executor framework helps run tasks in a managed thread pool without manually creating threads. Two common ways to run tasks are execute() and submit(), which differ in result handling and exception management.

  • execute(): Runs a task without returning any result; exceptions may be lost.
  • submit(): Runs a task and returns a Future, letting you get the result or handle exceptions.

Use Case Example:

Java
ExecutorService es = Executors.newFixedThreadPool(2);

// execute() - fire-and-forget
es.execute(() -> System.out.println("Task executed"));

// submit() - get result or exception
Future<Integer> f = es.submit(() -> { throw new RuntimeException("Error"); });
try { f.get(); } catch (ExecutionException e) { System.out.println("Caught: " + e.getCause()); }

es.shutdown();

2. Explain the lifecycle of an ExecutorService. How do shutdown() and shutdownNow() differ, and what are their consequences?

An ExecutorService is a high-level API in Java that manages a pool of threads to run tasks efficiently. Knowing its lifecycle and shutdown methods is important to avoid leaving threads running or losing tasks.

  • shutdown(): Stops accepting new tasks but allows already submitted tasks to complete.
  • shutdownNow(): Tries to stop all running tasks immediately and returns tasks that were waiting to execute.
Java
ExecutorService es = Executors.newFixedThreadPool(2);

es.submit(() -> System.out.println("Task 1"));
es.submit(() -> System.out.println("Task 2"));

// Graceful shutdown
es.shutdown(); // waits for tasks to finish

// Forceful shutdown
// List<Runnable> pending = es.shutdownNow(); // stops running tasks immediately

Pitfall: Calling shutdownNow() may interrupt important computation and lead to inconsistency if not handled.

3. How does Callable differ from Runnable? How do you retrieve results and handle exceptions from a Callable?

In Java, threads can perform tasks using Runnable or Callable. While both allow running code in parallel, Callable is more powerful because it can return results and throw checked exceptions, making it ideal for tasks where you need a response or error handling.

  • Runnable: Cannot return a result and cannot throw checked exceptions.
  • Callable: Returns a result (Future) and can throw exceptions.

Example:

Java
ExecutorService es = Executors.newFixedThreadPool(2);

// Callable task
Callable<String> task = () -> {
    if (new Random().nextBoolean()) throw new IOException("Error");
    return "Task Completed";
};

// Submit and get result
Future<String> future = es.submit(task);

try {
    String result = future.get(); // blocks until result is ready
    System.out.println(result);
} catch (ExecutionException e) {
    System.out.println("Handled Exception: " + e.getCause());
} catch (InterruptedException e) {
    e.printStackTrace();
}

es.shutdown();

4. How is a ThreadPoolExecutor configured in Java? How can misconfiguration lead to thread starvation or memory leaks?

A ThreadPoolExecutor allows you to manage a pool of threads efficiently, controlling how tasks are queued and executed. Proper configuration is essential to balance performance, avoid resource wastage, and prevent thread starvation or memory issues.

A ThreadPoolExecutor is configured using parameters like:

  • corePoolSize: Minimum number of threads to keep alive.
  • maximumPoolSize: Maximum number of threads allowed.
  • keepAliveTime: Time extra threads wait before termination.
  • workQueue: Queue to hold tasks before execution.
  • rejectionPolicy: Action when the queue is full.

Example:

Java
ExecutorService pool = new ThreadPoolExecutor(
    2, 4, 10, TimeUnit.SECONDS, new LinkedBlockingQueue<>(100),
    Executors.defaultThreadFactory(),
    new ThreadPoolExecutor.AbortPolicy() // rejects new tasks if full
);

// Submitting tasks
for (int i = 0; i < 5; i++) {
    int taskId = i;
    pool.submit(() -> System.out.println("Executing task " + taskId));
}

pool.shutdown();

5. What is the role of ScheduledExecutorService? How is scheduleAtFixedRate different from scheduleWithFixedDelay?

ScheduledExecutorService allows you to schedule tasks to run after a delay or repeatedly at fixed intervals, making it ideal for periodic or delayed tasks.

  • scheduleAtFixedRate: Runs tasks at fixed intervals, measured from the start of the previous execution.
  • scheduleWithFixedDelay: Runs tasks with a fixed delay after the previous execution completes.

Example:

Java
ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);

// Fixed rate: runs every 5 seconds from start time
scheduler.scheduleAtFixedRate(() -> System.out.println("FixedRate Task"), 0, 5, TimeUnit.SECONDS);

// Fixed delay: runs 5 seconds after previous task completes
scheduler.scheduleWithFixedDelay(() -> System.out.println("FixedDelay Task"), 0, 5, TimeUnit.SECONDS);

Use case: FixedRate for heartbeats, FixedDelay for log processing

6. What is the Fork/Join framework in Java? How does RecursiveTask enable parallelism? Give an example.

The Fork/Join framework is used to split large tasks into smaller subtasks that run in parallel and then combine the results. RecursiveTask allows subtasks to return results which are merged to get the final output.

class SumTask extends RecursiveTask<Integer> { /* ... */ }

int sum = new ForkJoinPool().invoke(new SumTask(arr, 0, arr.length));

7. Explain the difference between synchronized and ReentrantLock. When should you prefer explicit locks?

In Java, both synchronized and ReentrantLock are used to achieve mutual exclusion, but ReentrantLock provides more advanced features for flexible concurrency control.

FeaturesynchronizedReentrantLock
InterruptibleNoYes
Try lockNoYes (tryLock())
Fairness controlNoYes
Condition objectNoYes

When to use ReentrantLock:

  • You need timeout or interruptible locks
  • You want fairness (FIFO order for threads)
  • You need multiple condition variables for complex coordination

8. What is the use of tryLock() in concurrent programming? How can it prevent deadlocks?

In Java, tryLock() is an advanced feature of ReentrantLock that tries to acquire a lock without blocking the thread. If the lock is unavailable, the thread can retry later or take an alternative action, rather than waiting indefinitely.

Example:

Java
ReentrantLock lock1 = new ReentrantLock();
ReentrantLock lock2 = new ReentrantLock();

if(lock1.tryLock()) {
    try {
        if(lock2.tryLock()) {
            try {
                // safe to execute critical section
            } finally { lock2.unlock(); }
        }
    } finally { lock1.unlock(); }
}

How it prevents deadlocks:

  • Avoids circular waits because a thread won’t block indefinitely on a lock.
  • Allows retry or backoff strategies for safe concurrency.

9. Compare ReadWriteLock and StampedLock. How does optimistic reading in StampedLock improve performance?

In Java, both ReadWriteLock and StampedLock allow multiple threads to read shared data concurrently while ensuring exclusive access for writers.

FeatureReadWriteLockStampedLock
Read lock typeBlockingOptimistic
Upgrading lockNoYes (carefully)

Example:

Java
StampedLock lock = new StampedLock();
long stamp = lock.tryOptimisticRead();
int value = sharedData;
if (!lock.validate(stamp)) { // fallback if write occurred
    stamp = lock.readLock();
    try { value = sharedData; } finally { lock.unlockRead(stamp); }
}

Optimistic reads reduce contention, especially in read-heavy systems.

10. What is CountDownLatch? How is it different from CyclicBarrier and Phaser?

In Java, CountDownLatch is a synchronization tool that allows threads to wait until a set of operations in other threads completes. It has a fixed count and cannot be reused once the count reaches zero.

  • CountDownLatch: Threads wait until the counter reaches zero; not reusable.
  • CyclicBarrier: Threads wait at a barrier point; reusable after barrier is tripped.
  • Phaser: Advanced multi-phase synchronization; reusable and supports dynamic registration of threads.

Example:

Java
CountDownLatch latch = new CountDownLatch(3);

new Thread(() -> { /* task */ latch.countDown(); }).start();
latch.await(); // main thread waits for all tasks

11. How does a Semaphore control access to a resource? Give a real-world example.

In Java, a Semaphore is a concurrency tool that controls access to a limited number of resources by allowing only a fixed number of threads to acquire the permit at a time.

  • Semaphore(n): Allows up to n threads to access the resource concurrently.
  • Threads must acquire() a permit before using the resource and release() it afterward.

Example:

Java
Semaphore dbConnections = new Semaphore(5);

dbConnections.acquire(); // acquire permit
// perform DB operation
dbConnections.release(); // release permit

Use Case: DB pool, printer access, API rate limiting

12. What are atomic classes in Java and how do they differ from locks? Explain CAS (Compare-And-Swap) with AtomicInteger.

In Java, atomic classes (like AtomicInteger, AtomicLong) provide thread-safe, lock-free operations on single variables using Compare-And-Swap (CAS). They ensure updates are atomic without blocking threads, unlike traditional locks.

  • CAS (Compare-And-Swap): Checks the current value and updates it only if it matches the expected value, preventing race conditions.

Example:

Java
AtomicInteger ai = new AtomicInteger(10);
ai.compareAndSet(10, 20); // updates to 20 only if current value is 10

13. What problems can arise if AtomicInteger.incrementAndGet() is used along with non-atomic operations? Illustrate.

In multithreaded programming, AtomicInteger is often used to safely increment counters without explicit synchronization. However, even atomic operations can cause problems when combined with other non-atomic operations.

Example:

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

public class AtomicExample {
    static AtomicInteger counter = new AtomicInteger(0);
    static int nonAtomicValue = 0;

    public static void main(String[] args) {
        Runnable task = () -> {
            int temp = counter.incrementAndGet(); // atomic increment
            nonAtomicValue = temp * 2;           // non-atomic operation
            System.out.println(nonAtomicValue);
        };

        for (int i = 0; i < 5; i++) {
            new Thread(task).start();
        }
    }
}

Output
2
4
6
8
10

Explanation:

  • counter.incrementAndGet(): thread-safe
  • nonAtomicValue = temp * 2: not thread-safe
  • Multiple threads can overwrite nonAtomicValue simultaneously, causing inconsistent results.

How to Fix

Wrap the combined operation in a synchronized block:

Java
Runnable task = () -> {
    synchronized (AtomicExample.class) {
        int temp = counter.incrementAndGet();
        nonAtomicValue = temp * 2;
        System.out.println(nonAtomicValue);
    }
};

14. What is CompletableFuture? How does it support async programming in Java? Show task chaining and exception handling.

CompletableFuture is a Java class (from java.util.concurrent) that represents a future result of an asynchronous computation. You can start tasks in the background, chain multiple tasks together, and handle exceptions without blocking the main thread.

Example: Task Chaining

Java
import java.util.concurrent.CompletableFuture;

public class Example {
    public static void main(String[] args) {
        CompletableFuture.supplyAsync(() -> 5)
            .thenApply(x -> x * 2)
            .thenApply(x -> x + 3)
            .thenAccept(result -> System.out.println("Final Result: " + result));
    }
}

Output: Final Result: 13

Example: Exception Handling:

Java
CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> {
    if (true) throw new RuntimeException("Error!");
    return 10;
}).exceptionally(ex -> 0);

System.out.println(future.join()); // Output: 0

15. How do you combine multiple CompletableFutures and wait for all or any to complete?

In Java, CompletableFuture allows you to run multiple asynchronous tasks. Sometimes you need to wait for all tasks to finish or proceed as soon as any task completes. Java provides allOf() and anyOf() methods for this purpose.

1. Wait for All Tasks: CompletableFuture.allOf()

Java
import java.util.concurrent.CompletableFuture;

public class AllOfExample {
    public static void main(String[] args) {
        CompletableFuture<String> f1 = CompletableFuture.supplyAsync(() -> "Task 1");
        CompletableFuture<String> f2 = CompletableFuture.supplyAsync(() -> "Task 2");
        CompletableFuture<String> f3 = CompletableFuture.supplyAsync(() -> "Task 3");

        // Wait for all to complete
        CompletableFuture<Void> all = CompletableFuture.allOf(f1, f2, f3);

        all.join(); // blocks until all tasks complete
        System.out.println(f1.join() + ", " + f2.join() + ", " + f3.join());
    }
}

Output
Task 1, Task 2, Task 3
  • allOf() returns a CompletableFuture<Void> that completes when all provided futures complete.
  • You can then retrieve individual results using .join().

2. Wait for Any Task: CompletableFuture.anyOf()

Java
import java.util.concurrent.CompletableFuture;

public class AnyOfExample {
    public static void main(String[] args) {
        CompletableFuture<String> f1 = CompletableFuture.supplyAsync(() -> "Fast Task");
        CompletableFuture<String> f2 = CompletableFuture.supplyAsync(() -> {
            try { Thread.sleep(1000); } catch (InterruptedException e) {}
            return "Slow Task";
        });

        // Wait for any task to complete
        CompletableFuture<Object> any = CompletableFuture.anyOf(f1, f2);

        System.out.println("First completed: " + any.join());
    }
}

Output
First completed: Fast Task

anyOf() returns a CompletableFuture<Object> that completes as soon as any one of the futures completes.

Comment