Concurrency questions separate mid-level from senior Java developers. Know thread lifecycle, synchronization mechanisms, and modern alternatives like virtual threads.

Thread Basics

Q: Ways to create a thread in Java?

  // 1. Extend Thread (avoid — limits inheritance)
class MyThread extends Thread {
    public void run() { System.out.println("Running"); }
}

// 2. Implement Runnable (preferred)
Thread t = new Thread(() -> System.out.println("Running"));

// 3. Callable + Future (returns result)
ExecutorService pool = Executors.newSingleThreadExecutor();
Future<Integer> future = pool.submit(() -> 42);
future.get(); // blocks until result

// 4. Virtual threads (Java 21+)
Thread vThread = Thread.ofVirtual().start(() -> System.out.println("Virtual"));
  

Q: Thread lifecycle states?

  NEW → RUNNABLE → RUNNING → TERMINATED
         ↕            ↓
      BLOCKED    WAITING / TIMED_WAITING
  
  • NEW — created, not started
  • RUNNABLE — eligible to run
  • BLOCKED — waiting for monitor lock
  • WAITING — wait(), join() with no timeout
  • TIMED_WAITING — sleep(), wait(timeout)
  • TERMINATED — run() completed

synchronized Keyword

Q: How does synchronized work?

  // Synchronized method — locks on this
public synchronized void increment() {
    count++;
}

// Synchronized block — locks on specific object
public void transfer(Account from, Account to, int amount) {
    synchronized (from) {
        synchronized (to) {
            from.debit(amount);
            to.credit(amount);
        }
    }
}
  

Only one thread holds the monitor at a time. Other threads block until release.

Q: Problems with synchronized?

  • No timeout on lock acquisition — potential deadlock
  • No interruptible lock waiting
  • No try-lock
  • No read/write lock separation

Use java.util.concurrent.locks for advanced control.

volatile Keyword

Q: What does volatile do?

  private volatile boolean running = true;

public void stop() {
    running = false;  // visible to all threads immediately
}
  
  • Guarantees visibility — changes written by one thread are seen by others
  • Prevents instruction reordering around volatile access
  • Does NOT provide atomicity for compound operations (count++ is not safe with volatile alone)

Use for single-writer flags. For counters, use AtomicInteger.

Atomic Classes

  AtomicInteger counter = new AtomicInteger(0);
counter.incrementAndGet();  // atomic, lock-free CAS
counter.compareAndSet(5, 10);  // CAS operation

AtomicReference<User> userRef = new AtomicReference<>();
userRef.updateAndGet(u -> new User(u.id(), "Updated"));
  

Based on Compare-And-Swap (CAS) — no blocking, better under low contention.

Locks

  ReentrantLock lock = new ReentrantLock();

lock.lock();
try {
    // critical section
} finally {
    lock.unlock();  // ALWAYS in finally
}

// Try lock with timeout — avoids deadlock
if (lock.tryLock(1, TimeUnit.SECONDS)) {
    try { /* work */ } finally { lock.unlock(); }
}

// ReadWriteLock — multiple readers OR one writer
ReadWriteLock rwLock = new ReentrantReadWriteLock();
rwLock.readLock().lock();   // shared
rwLock.writeLock().lock();  // exclusive
  

Thread Pools

Q: Why use thread pools instead of creating threads directly?

Creating threads is expensive (1MB stack, OS scheduling). Pools reuse threads:

  ExecutorService pool = Executors.newFixedThreadPool(4);

for (int i = 0; i < 100; i++) {
    int taskId = i;
    pool.submit(() -> processTask(taskId));
}

pool.shutdown();
pool.awaitTermination(1, TimeUnit.MINUTES);
  
Pool Type Use Case
FixedThreadPool Steady workload, limit concurrency
CachedThreadPool Short-lived, variable load
ScheduledThreadPool Delayed/periodic tasks
SingleThreadExecutor Sequential task processing

Production: configure ThreadPoolExecutor explicitly — avoid unbounded CachedThreadPool:

  ThreadPoolExecutor executor = new ThreadPoolExecutor(
    4,                          // core pool size
    8,                          // max pool size
    60L, TimeUnit.SECONDS,      // keep-alive
    new LinkedBlockingQueue<>(100),  // bounded queue
    new ThreadPoolExecutor.CallerRunsPolicy()  // rejection policy
);
  

CompletableFuture

Q: CompletableFuture vs Future?

  CompletableFuture<String> future = CompletableFuture
    .supplyAsync(() -> fetchUserData())       // async supply
    .thenApply(user -> user.name())           // transform
    .thenCompose(name -> fetchOrders(name))   // chain async
    .exceptionally(ex -> "default");          // handle error

// Combine multiple futures
CompletableFuture<String> f1 = CompletableFuture.supplyAsync(() -> "Hello");
CompletableFuture<String> f2 = CompletableFuture.supplyAsync(() -> "World");
CompletableFuture<String> combined = f1.thenCombine(f2, (a, b) -> a + " " + b);
  

Non-blocking composition — essential for reactive-style Java.

Virtual Threads (Java 21+)

Q: Virtual threads vs platform threads?

Platform Thread Virtual Thread
Backed by OS thread (1:1) JVM scheduler (M:N)
Memory ~1 MB stack ~1 KB
Count Thousands max Millions
Blocking I/O Wastes OS thread Carrier thread released
  // Create millions of virtual threads for I/O-bound work
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
    for (int i = 0; i < 100_000; i++) {
        int id = i;
        executor.submit(() -> fetchUrl(id));  // blocking I/O is fine
    }
}
  

Use virtual threads for I/O-bound workloads. CPU-bound work still needs platform threads or fork/join.

Common Concurrency Problems

Q: What is deadlock? How to prevent?

  // Deadlock — Thread 1 locks A then B, Thread 2 locks B then A
synchronized (lockA) {
    synchronized (lockB) { /* ... */ }
}
  

Prevention:

  • Lock ordering — always acquire locks in same order
  • Lock timeout — tryLock(timeout)
  • Reduce lock scope — hold locks for minimum time
  • Use concurrent collections instead of synchronized wrappers

Q: What is livelock vs starvation?

  • Deadlock — threads blocked forever waiting for each other
  • Livelock — threads actively responding to each other but making no progress
  • Starvation — thread never gets CPU/lock due to higher-priority threads

Concurrent Collections

  ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();
map.put("key", 1);
map.compute("key", (k, v) -> v == null ? 1 : v + 1);

BlockingQueue<Task> queue = new LinkedBlockingQueue<>(100);
queue.put(task);   // blocks if full
Task t = queue.take();  // blocks if empty

CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>();
// Expensive writes, cheap reads — good for read-heavy scenarios
  

Interview Coding: Print Odd/Even with Two Threads

  public class OddEven {
    private int count = 1;
    private final int max = 20;
    private final Object lock = new Object();

    public void printOdd() {
        synchronized (lock) {
            while (count <= max) {
                if (count % 2 == 0) lock.wait();
                else {
                    System.out.println("Odd: " + count++);
                    lock.notify();
                }
            }
        }
    }

    public void printEven() {
        synchronized (lock) {
            while (count <= max) {
                if (count % 2 != 0) lock.wait();
                else {
                    System.out.println("Even: " + count++);
                    lock.notify();
                }
            }
        }
    }
}
  

Modern alternative: use Semaphore or BlockingQueue.

Key Takeaways

  • Prefer high-level concurrency utilities over raw wait()/notify()
  • Use thread pools — never unbounded thread creation
  • ConcurrentHashMap over Collections.synchronizedMap()
  • Virtual threads for I/O-bound; platform threads for CPU-bound
  • Always handle InterruptedException properly — restore interrupt flag
  • Test concurrent code — race conditions are non-deterministic

Review Common Questions and Collections to complete interview prep.