Java multithreading and concurrency is one of the most tested backend topics at Indian product companies. Whether for BFSI systems, payment processing, or high-throughput APIs, understanding Java's concurrency model is expected for senior Java roles. This guide covers Java multithreading interview questions for India in 2026.
Java threads and the memory model
Java concurrency fundamentals:
1. Process vs thread: Process: an independent program with its own memory space (heap, stack, code segment, data segment). Context switching between processes is expensive. Thread: a lightweight unit of execution within a process. Threads in the same process share the heap (and thus can share objects) but have separate stacks. Context switching between threads is cheaper than between processes.
2. Creating threads in Java: - Extending Thread: class MyThread extends Thread { @Override public void run() { ... } }; new MyThread().start() - Implementing Runnable: class MyRunnable implements Runnable { @Override public void run() { ... } }; new Thread(new MyRunnable()).start() - Lambda (Java 8+): new Thread(() -> { ... }).start() - Callable + Future: for tasks that return a value or throw checked exceptions; submit to an ExecutorService.
3. Java Memory Model (JMM): The JMM defines when changes made by one thread to shared variables are visible to other threads. Without synchronisation, a thread may see stale values (cached in its local CPU cache or register). The JMM guarantees visibility across threads when: a thread exits a synchronized block, a thread reads a volatile variable, a thread starts a new thread (the child thread sees all writes by the parent before start()), a thread joins another thread (sees all writes by the finished thread).
4. Common concurrency problems: Race condition: two threads read-modify-write a shared variable without synchronisation; the final value depends on the execution order. Fix: synchronise the read-modify-write operation. Deadlock: thread A holds lock 1 and waits for lock 2; thread B holds lock 2 and waits for lock 1. Neither can proceed. Fix: acquire locks in a consistent order; use tryLock() with a timeout. Livelock: two threads keep responding to each other's actions without making progress (like two people stepping aside simultaneously to let the other pass). Starvation: a thread never gets CPU time because other threads with higher priority or more lock contention always run first.
synchronized, volatile, and atomic classes
Java synchronisation primitives:
1. synchronized keyword: The synchronized keyword provides mutual exclusion (only one thread can execute the synchronised block or method at a time) and visibility (all changes made inside the block are visible to the next thread that enters a synchronised block on the same object). Synchronised method: `synchronized void increment() { count++; }` — uses the object instance as the lock. Synchronised static method: uses the Class object as the lock. Synchronised block: `synchronized (lockObject) { ... }` — more fine-grained; use a dedicated lock object rather than this.
2. volatile keyword: A volatile variable is always read from and written to main memory (never cached in a thread's local cache). This ensures visibility: every write to a volatile variable by one thread is visible to all other threads immediately. volatile does NOT ensure atomicity: `count++` is a read-modify-write operation; even if count is volatile, two threads can still interleave their reads and writes. Use volatile for: simple flags (boolean done = false; set to true by one thread; read by another), and for the double-checked locking idiom (the volatile keyword prevents the compiler from reordering the object creation and the reference assignment).
3. Atomic classes (java.util.concurrent.atomic): AtomicInteger, AtomicLong, AtomicBoolean, AtomicReference: provide atomic operations without synchronisation, using hardware-level compare-and-swap (CAS) operations. `counter.incrementAndGet()` is atomic: it reads the value, increments, and writes back in one atomic operation. Much faster than synchronized for simple counter operations.
4. Lock interface (java.util.concurrent.locks): ReentrantLock: an explicit lock with more capabilities than synchronized. Features: tryLock() (try to acquire the lock without blocking; return false if not available), lockInterruptibly() (acquire the lock but allow the thread to be interrupted while waiting), separate Condition objects for fine-grained wait/notify. ReadWriteLock: allows multiple concurrent readers or one exclusive writer; improves throughput for read-heavy use cases.
ExecutorService and CompletableFuture
Java high-level concurrency:
1. ExecutorService: A higher-level thread management API. Instead of creating threads directly, submit tasks to an ExecutorService that manages a pool of threads. Common implementations: - Executors.newFixedThreadPool(n): a pool with a fixed number of threads; queues tasks when all threads are busy - Executors.newCachedThreadPool(): creates threads as needed; reuses idle threads; grows unboundedly (use with caution) - Executors.newSingleThreadExecutor(): exactly one thread; tasks are executed sequentially - Executors.newScheduledThreadPool(n): schedule tasks to run after a delay or periodically
Shutdown: always shut down the ExecutorService when done: shutdown() (stop accepting new tasks; wait for existing tasks to complete), shutdownNow() (attempt to cancel running tasks; return unstarted tasks).
2. Future and Callable: Callable<T>: like Runnable but returns a value and can throw checked exceptions. Submit to ExecutorService: Future<T> future = executor.submit(callable). Future.get(): blocks until the task completes and returns the result (or throws ExecutionException). Future.isDone(): non-blocking check if the task has completed.
3. CompletableFuture (Java 8+): CompletableFuture<T> supports non-blocking, composable asynchronous programming: ```java CompletableFuture .supplyAsync(() -> fetchUser(userId)) // async: runs on ForkJoinPool .thenApply(user -> enrichUser(user)) // transform the result .thenCompose(user -> fetchOrders(user.getId())) // chain another async operation .thenAccept(orders -> notifyUser(orders)) // consume the result .exceptionally(ex -> { log.error('Failed', ex); return null; }) // handle errors ``` Combining: CompletableFuture.allOf(f1, f2, f3) — wait for all to complete. CompletableFuture.anyOf(f1, f2) — complete when the first finishes.
4. ForkJoinPool: A thread pool designed for recursive, divide-and-conquer tasks (like parallel merge sort or parallel stream processing). The default pool for CompletableFuture.supplyAsync() and Java parallel streams.
Practise Java multithreading and concurrency interview questions with HireStepX's AI voice interviewer. Get scored feedback on your explanations of synchronisation, thread safety, and concurrent data structures. First 2 sessions free.
Practice freeConcurrentHashMap, thread safety, and common patterns
Java concurrency patterns:
1. Thread-safe collections: ConcurrentHashMap: thread-safe HashMap that allows concurrent reads and partial concurrent writes (segments the map; different segments can be written concurrently). Better throughput than a synchronised HashMap. CopyOnWriteArrayList: thread-safe list that creates a new copy on every write; read is lock-free; use for read-heavy, write-rare lists. BlockingQueue (LinkedBlockingQueue, ArrayBlockingQueue): thread-safe queue where put() blocks if the queue is full and take() blocks if the queue is empty. Classic producer-consumer implementation.
2. Producer-consumer pattern: ```java BlockingQueue<Task> queue = new LinkedBlockingQueue<>(100);
// Producer thread: void produce() throws InterruptedException { while (true) { queue.put(new Task()); // blocks if queue is full } }
// Consumer thread: void consume() throws InterruptedException { while (true) { Task task = queue.take(); // blocks if queue is empty process(task); } } ```
3. CountDownLatch and CyclicBarrier: CountDownLatch: one or more threads wait until a count reaches zero. Example: the main thread waits for 5 worker threads to complete initialisation before starting. CyclicBarrier: all participating threads wait at a barrier point until all have arrived; then all proceed. Example: all threads must complete Phase 1 before any can start Phase 2. CyclicBarrier is reusable; CountDownLatch is not.
4. Semaphore: Controls the number of threads that can access a shared resource concurrently. Example: limit database connection pool to 10 connections — semaphore with 10 permits; acquire a permit before getting a connection; release after returning it. Also useful for rate limiting within a JVM.
Frequently asked questions
Explore more