FroquizFroquiz
HomeQuizzesSenior ChallengeGet CertifiedBlogAbout
Sign InStart Quiz
Sign InStart Quiz
Froquiz

The most comprehensive quiz platform for software engineers. Test yourself with 10000+ questions and advance your career.

LinkedIn

Platform

  • Start Quizzes
  • Topics
  • Blog
  • My Profile
  • Sign In

About

  • About Us
  • Contact

Legal

  • Privacy Policy
  • Terms of Service

Β© 2026 Froquiz. All rights reserved.Built with passion for technology
Blog & Articles

Java Multithreading and Concurrency: A Complete Interview Guide

Master Java concurrency with clear examples. Covers threads, Runnable, synchronized, volatile, locks, ExecutorService, CompletableFuture, and common concurrency pitfalls.

Yusuf SeyitoğluMarch 11, 20261 views12 min read

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

java
public 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)

java
public 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:

java
public 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:

java
public 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:

java
public 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.

volatile ensures visibility but not atomicity. For count++, you still need synchronized or AtomicInteger.

Atomic Classes

java.util.concurrent.atomic provides lock-free thread-safe operations:

java
import 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:

java
import 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:

java
import 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

java
ScheduledExecutorService 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:

java
import 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 Runnable or lambdas, not Thread subclasses, for tasks
  • synchronized provides mutual exclusion and visibility
  • volatile provides visibility only β€” not sufficient for compound operations
  • AtomicInteger and friends provide lock-free atomic operations
  • Use thread pools via ExecutorService β€” never create threads manually per task
  • CompletableFuture enables composable, non-blocking async pipelines
  • Use ConcurrentHashMap, CopyOnWriteArrayList, BlockingQueue for thread-safe collections
  • Prevent deadlocks by acquiring locks in a consistent order

About Author

Yusuf Seyitoğlu

Author β†’

Other Posts

  • CSS Advanced Techniques: Custom Properties, Container Queries, Grid Masonry and Modern LayoutsMar 12
  • Java Collections Deep Dive: ArrayList, HashMap, TreeMap, LinkedHashMap and When to Use EachMar 12
  • GraphQL Schema Design: Types, Resolvers, Mutations and Best PracticesMar 12
All Blogs