Java Multithreading and Concurrency: A Complete Interview Guide
Java concurrency is one of the most challenging and most frequently tested topics in senior Java interviews. Understanding threads, synchronization, and the modern concurrency utilities separates developers who write safe, performant multi-threaded code from those who introduce subtle, hard-to-reproduce bugs.
Why Concurrency Is Hard
Multiple threads accessing shared mutable state without coordination leads to:
- Race conditions β outcome depends on thread scheduling order
- Deadlocks β threads wait for each other forever
- Memory visibility issues β changes made by one thread not visible to others
The solution is careful synchronization β but over-synchronization kills performance.
Creating Threads
Extending Thread
javapublic class MyThread extends Thread { @Override public void run() { System.out.println("Running in: " + Thread.currentThread().getName()); } } MyThread t = new MyThread(); t.start(); // start() creates a new thread; run() would execute on current thread
Implementing Runnable (preferred)
javapublic class MyTask implements Runnable { @Override public void run() { System.out.println("Task running"); } } Thread t = new Thread(new MyTask()); t.start(); // Lambda version Thread t = new Thread(() -> System.out.println("Lambda task")); t.start();
Prefer Runnable over extending Thread β it separates the task from the thread mechanism, and your class can still extend another class.
The synchronized Keyword
synchronized prevents multiple threads from executing a block simultaneously:
javapublic class Counter { private int count = 0; // Synchronized method -- one thread at a time public synchronized void increment() { count++; } public synchronized int getCount() { return count; } }
Without synchronized, count++ is not atomic β it is three operations (read, increment, write) and two threads can interleave, causing lost updates.
Synchronized block
More granular than a synchronized method:
javapublic class Cache { private final Map<String, Object> store = new HashMap<>(); private final Object lock = new Object(); public void put(String key, Object value) { synchronized (lock) { store.put(key, value); } } public Object get(String key) { synchronized (lock) { return store.get(key); } } }
volatile
volatile guarantees that reads and writes to a variable are visible to all threads immediately β it disables CPU caching and reordering for that variable:
javapublic class ServerStatus { private volatile boolean running = true; public void stop() { running = false; // immediately visible to all threads } public void serve() { while (running) { // process requests } } }
Without volatile, the while (running) loop might cache running in a CPU register and never see the update from another thread.
volatileensures visibility but not atomicity. Forcount++, you still needsynchronizedorAtomicInteger.
Atomic Classes
java.util.concurrent.atomic provides lock-free thread-safe operations:
javaimport java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; AtomicInteger count = new AtomicInteger(0); count.incrementAndGet(); // atomic increment, returns new value count.getAndIncrement(); // atomic increment, returns old value count.compareAndSet(5, 10); // if current value is 5, set to 10 // AtomicReference for object references AtomicReference<String> ref = new AtomicReference<>("initial"); ref.compareAndSet("initial", "updated");
Atomic classes use CPU-level compare-and-swap (CAS) instructions β faster than synchronized for simple operations.
java.util.concurrent Locks
The Lock interface provides more flexibility than synchronized:
javaimport java.util.concurrent.locks.ReentrantLock; import java.util.concurrent.locks.ReadWriteLock; import java.util.concurrent.locks.ReentrantReadWriteLock; // ReentrantLock ReentrantLock lock = new ReentrantLock(); lock.lock(); try { // critical section } finally { lock.unlock(); // always release in finally } // Try to acquire without blocking if (lock.tryLock()) { try { // got the lock } finally { lock.unlock(); } } else { // could not acquire lock, do something else } // ReadWriteLock -- multiple readers OR one writer ReadWriteLock rwLock = new ReentrantReadWriteLock(); rwLock.readLock().lock(); // multiple threads can hold this try { /* read */ } finally { rwLock.readLock().unlock(); } rwLock.writeLock().lock(); // exclusive try { /* write */ } finally { rwLock.writeLock().unlock(); }
ExecutorService and Thread Pools
Creating a new thread for every task is expensive. Thread pools reuse threads:
javaimport java.util.concurrent.*; // Fixed thread pool -- exactly N threads ExecutorService executor = Executors.newFixedThreadPool(4); // Cached thread pool -- creates threads as needed, reuses idle ones ExecutorService executor = Executors.newCachedThreadPool(); // Single thread executor -- tasks run sequentially ExecutorService executor = Executors.newSingleThreadExecutor(); // Submit a Runnable (no result) executor.submit(() -> processOrder(order)); // Submit a Callable (returns a result) Future<Integer> future = executor.submit(() -> { return computeExpensiveValue(); }); // Get the result (blocks until done) int result = future.get(); int result = future.get(5, TimeUnit.SECONDS); // with timeout // Shutdown gracefully executor.shutdown(); executor.awaitTermination(10, TimeUnit.SECONDS);
Modern approach: ScheduledExecutorService
javaScheduledExecutorService scheduler = Executors.newScheduledThreadPool(2); // Run after a delay scheduler.schedule(() -> sendReminder(), 30, TimeUnit.MINUTES); // Run repeatedly scheduler.scheduleAtFixedRate( () -> checkHealth(), 0, // initial delay 60, // period TimeUnit.SECONDS );
CompletableFuture
Java 8 introduced CompletableFuture for composable async programming:
javaimport java.util.concurrent.CompletableFuture; // Run asynchronously CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> { return fetchUserFromDb(userId); }); // Chain operations CompletableFuture<String> result = CompletableFuture .supplyAsync(() -> fetchUser(userId)) .thenApply(user -> enrichWithProfile(user)) .thenApply(user -> user.toDisplayString()) .exceptionally(ex -> "Error: " + ex.getMessage()); // Combine multiple futures CompletableFuture<User> userFuture = CompletableFuture.supplyAsync(() -> fetchUser(id)); CompletableFuture<Profile> profileFuture = CompletableFuture.supplyAsync(() -> fetchProfile(id)); CompletableFuture<UserWithProfile> combined = userFuture .thenCombine(profileFuture, (user, profile) -> new UserWithProfile(user, profile)); // Wait for all to complete CompletableFuture.allOf(future1, future2, future3).join();
Thread-Safe Collections
Never use non-thread-safe collections (HashMap, ArrayList) from multiple threads. Use these instead:
java// Concurrent HashMap -- fine-grained locking, high throughput Map<String, Integer> map = new ConcurrentHashMap<>(); // CopyOnWriteArrayList -- reads without locking, writes copy the list List<String> list = new CopyOnWriteArrayList<>(); // BlockingQueue -- for producer-consumer patterns BlockingQueue<Task> queue = new LinkedBlockingQueue<>(100); queue.put(task); // blocks if full Task task = queue.take(); // blocks if empty queue.offer(task, 5, TimeUnit.SECONDS); // with timeout
Common Concurrency Problems
Deadlock
Two threads each wait for the other to release a lock:
java// Thread 1: locks A then tries to lock B // Thread 2: locks B then tries to lock A // Neither can proceed -- deadlock // Prevention: always acquire locks in the same order // Always lock A before B, everywhere in the codebase
Race Condition Example
java// Not thread-safe -- check-then-act is two operations if (!map.containsKey(key)) { map.put(key, value); // another thread may have inserted between check and put } // Thread-safe alternative map.putIfAbsent(key, value); // atomic operation on ConcurrentHashMap
Common Interview Questions
Q: What is the difference between synchronized and volatile?
synchronized ensures both visibility and atomicity β only one thread executes the block at a time. volatile ensures visibility only β all threads see the latest value, but compound operations like count++ are still not atomic.
Q: What is a deadlock and how do you prevent it?
A deadlock occurs when two or more threads wait for each other to release locks they hold. Prevention: always acquire multiple locks in the same predefined order, use tryLock with timeouts, or use higher-level concurrency utilities that manage locking internally.
Q: What is the difference between Future and CompletableFuture?
Future is limited β you can only block and wait for the result with get(). CompletableFuture is composable β you can chain operations with thenApply, thenCompose, thenCombine, handle errors with exceptionally, and combine multiple futures with allOf without blocking.
Q: When would you use CopyOnWriteArrayList?
When reads vastly outnumber writes and you need thread-safe iteration. Reads never lock. Writes create a full copy of the underlying array, making them expensive. Ideal for rarely-modified read-mostly lists like event listener registries.
Practice Java on Froquiz
Concurrency is tested in intermediate and senior Java interviews across industries. Test your Java knowledge on Froquiz β covering threading, collections, OOP, and advanced topics.
Summary
- Use
Runnableor lambdas, notThreadsubclasses, for tasks synchronizedprovides mutual exclusion and visibilityvolatileprovides visibility only β not sufficient for compound operationsAtomicIntegerand friends provide lock-free atomic operations- Use thread pools via
ExecutorServiceβ never create threads manually per task CompletableFutureenables composable, non-blocking async pipelines- Use
ConcurrentHashMap,CopyOnWriteArrayList,BlockingQueuefor thread-safe collections - Prevent deadlocks by acquiring locks in a consistent order