Java Multithreading Interview Questions for Experienced Developers
|

Java Multithreading Interview Questions for Experienced Developers

Java Multithreading Interview Questions for Experienced Developers

Java Multithreading Interview Questions for Experienced Developers

⏱️ 21 min read | 📚 Updated June 2026

💡 Quick Tip: Need fast answers? Jump directly to the FAQ section below.

View Quick Answers ↓

If you’re preparing for senior Java developer interviews at tier-1 companies like TCS, Infosys, Wipro, or EPAM in 2026, multithreading is no longer a “nice-to-know” topic—it’s a must-know. I’ve reviewed hundreds of interview questions and candidate responses, and I can tell you that multithreading knowledge separates mid-level developers from senior architects.

In this guide, you’ll encounter the exact multithreading questions that experienced developers face in real interviews, complete with production-grade answers and working code examples. We’ll cover advanced topics like thread safety, synchronization mechanisms, the memory model, and concurrency utilities that go beyond the basics.

You’ll learn not just the “what” but the “why” behind each answer, enabling you to explain your reasoning confidently in interviews. Let’s dive in.

Table of Contents

Why Interviewers Ask These Questions

Java Multithreading Interview Questions for Experienced Developers
Quick visual comparison — Java Multithreading Interview Questions for Experienced Developers

Multithreading is fundamental to building scalable applications. At companies like Infosys and TCS, you’ll work on systems handling millions of concurrent requests. Interviewers ask these questions to assess whether you can design thread-safe systems, understand performance implications, and avoid subtle bugs like race conditions and deadlocks.

The questions follow a progression: they start with basic concepts (thread creation, synchronization) and escalate to real-world scenarios (designing a thread-safe cache, handling high-concurrency workloads, optimizing lock contention). An experienced developer should demonstrate not just theoretical knowledge but practical understanding of when and how to apply each pattern.

Key assessment areas:

  • Thread lifecycle and state transitions
  • Synchronization mechanisms and their trade-offs
  • Java Memory Model and visibility guarantees
  • Concurrency utilities (ExecutorService, CountDownLatch, Semaphore, etc.)
  • Deadlock detection and prevention
  • Performance optimization under high concurrency

Quick Comparison: Synchronization Mechanisms

Mechanism Use Case Performance Fairness Reentrancy
synchronized Simple critical sections Medium (biased locking optimization) No Yes
ReentrantLock Complex locking patterns, fair queuing High (explicit control) Yes (optional) Yes
ReadWriteLock Read-heavy workloads High (concurrent reads) Yes Yes
StampedLock Ultra-high concurrency, read-heavy Very High (optimistic reads) No No
AtomicXXX Single variable updates Very High (CAS operations) No No

Understanding Java Memory Model

The Java Memory Model (JMM) is often misunderstood, but it’s critical for writing correct multithreaded code. The JMM defines the visibility and ordering guarantees between threads. Without understanding it, you’ll write code that works on your machine but fails in production due to CPU optimizations and compiler reordering.

The key concept is the happens-before relationship. If action A happens-before action B, then all memory changes made in A are visible to B. For example:

  • Monitor lock rule: If you release a lock, all changes are visible to the next thread acquiring it.
  • Volatile field rule: Writing to a volatile field happens-before reading it from another thread.
  • Thread start rule: Calls to Thread.start() happen-before any action in the thread.
  • Thread termination rule: Any action in a thread happens-before the joining thread returns.

This is why volatile is different from synchronized: volatile only guarantees visibility, not atomicity. A volatile counter won’t protect against concurrent increments, but a volatile boolean flag (like a shutdown flag) is safe because reads and writes are atomic operations.

Thread Safety and Visibility

Thread safety means that multiple threads can access shared data without data corruption or inconsistent state. But achieving thread safety is more nuanced than just adding synchronized. You need to consider visibility, atomicity, and ordering.

Visibility: This is the hardest problem. Without proper synchronization, Thread B might not see changes made by Thread A due to CPU cache differences. The JMM uses happens-before guarantees to ensure visibility. In practice, this means:

  • Use synchronized or locks to protect shared mutable state.
  • Use volatile for simple flags or status values that don’t require atomicity.
  • Use final fields whenever possible—they’re inherently thread-safe.
  • Use immutable objects instead of synchronizing (SafePublisher pattern).

A practical example: if a non-volatile counter is incremented in one thread and read in another, the reading thread might see stale values. But if you declare the counter as volatile, reads are guaranteed to see the most recent write (though increments still aren’t atomic).

Advanced Concurrency Patterns

Senior developers are expected to know patterns beyond basic synchronization. These patterns solve real-world problems and demonstrate architectural thinking.

Double-Checked Locking

A common pattern for lazy initialization of expensive objects. The idea is to minimize lock contention by checking if initialization is needed before acquiring the lock.


public class ConfigurationManager {
    private volatile Configuration config = null;
    
    public Configuration getConfiguration() {
        if (config == null) {
            synchronized (this) {
                if (config == null) {
                    config = initializeConfiguration(); // Expensive operation
                }
            }
        }
        return config;
    }
    
    private Configuration initializeConfiguration() {
        // Simulate heavy initialization
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
        return new Configuration("loaded");
    }
}

The volatile keyword is crucial here. Without it, the JMM doesn’t guarantee visibility of the initialized object, and Thread B might see a partially constructed instance. This pattern is useful but should be replaced with lazy initialization utilities when available.

Producer-Consumer Pattern

A classic pattern where producers add items to a queue and consumers remove them. This requires careful synchronization to ensure thread safety and proper signaling between threads.


public class BoundedBuffer {
    private final T[] buffer;
    private int count = 0;
    private int in = 0;
    private int out = 0;
    private final Object notFull = new Object();
    private final Object notEmpty = new Object();
    
    @SuppressWarnings("unchecked")
    public BoundedBuffer(int size) {
        buffer = new T[size];
    }
    
    public void put(T item) throws InterruptedException {
        synchronized (notFull) {
            while (count == buffer.length) {
                notFull.wait();
            }
            buffer[in] = item;
            in = (in + 1) % buffer.length;
            count++;
            synchronized (notEmpty) {
                notEmpty.notifyAll();
            }
        }
    }
    
    public T take() throws InterruptedException {
        synchronized (notEmpty) {
            while (count == 0) {
                notEmpty.wait();
            }
            T item = buffer[out];
            out = (out + 1) % buffer.length;
            count--;
            synchronized (notFull) {
                notFull.notifyAll();
            }
            return item;
        }
    }
}

Note the separate lock objects for signaling. This allows producers and consumers to wait and notify independently, improving concurrency. In practice, use BlockingQueue instead, but understanding this pattern demonstrates mastery of wait/notify mechanics.

Read-Write Lock Pattern

When reads vastly outnumber writes, a ReadWriteLock improves concurrency by allowing multiple readers while ensuring exclusive writes.


import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

public class CachedData {
    private final ReadWriteLock lock = new ReentrantReadWriteLock();
    private String data;
    
    public String read() {
        lock.readLock().lock();
        try {
            return data;
        } finally {
            lock.readLock().unlock();
        }
    }
    
    public void write(String newData) {
        lock.writeLock().lock();
        try {
            this.data = newData;
        } finally {
            lock.writeLock().unlock();
        }
    }
}

With 100 reader threads and 1 writer, a ReadWriteLock dramatically outperforms synchronized. Readers acquire locks concurrently, but the writer waits for all readers to finish.

Performance and Optimization

Understanding performance characteristics separates experienced developers from novices. Lock contention is often the bottleneck in multithreaded systems.

Biased locking: The JVM optimizes synchronized blocks by assuming a single thread will repeatedly acquire the lock (biased). This reduces the cost to a simple comparison. When another thread arrives, the lock is “unbiased,” incurring a one-time cost. This optimization is transparent but important to understand.

Lock striping: Instead of protecting an entire collection with one lock, divide it into segments and use separate locks for each. For example, ConcurrentHashMap uses this approach, allowing concurrent writes to different segments. You can implement this pattern manually:


public class StripedMap<K, V> {
    private static final int STRIPES = 16;
    private final Object[] locks = new Object[STRIPES];
    private final Map<K, V>[] maps = new HashMap[STRIPES];
    
    @SuppressWarnings("unchecked")
    public StripedMap() {
        for (int i = 0; i < STRIPES; i++) {
            locks[i] = new Object();
            maps[i] = new HashMap<>();
        }
    }
    
    private int getStripe(K key) {
        return Math.abs(key.hashCode() % STRIPES);
    }
    
    public void put(K key, V value) {
        int stripe = getStripe(key);
        synchronized (locks[stripe]) {
            maps[stripe].put(key, value);
        }
    }
    
    public V get(K key) {
        int stripe = getStripe(key);
        synchronized (locks[stripe]) {
            return maps[stripe].get(key);
        }
    }
}

This approach reduces lock contention by a factor of STRIPES, especially under high concurrency. Real-world systems use this pattern extensively.

Top Interview Questions & Answers

Q1: What’s the difference between extending Thread and implementing Runnable?

Answer: Extending Thread directly means you inherit from a concrete class, limiting you to single inheritance. Implementing Runnable is more flexible because Java allows multiple interface implementations. Additionally, if your class already extends another class (e.g., Activity in Android), you can’t extend Thread, but you can implement Runnable. The modern approach is to implement Runnable and pass it to the Thread constructor or, better yet, use ExecutorService for thread management. From a design perspective, separating the task (Runnable) from the execution mechanism (Thread/ExecutorService) follows the separation of concerns principle.

Q2: Explain the difference between volatile and synchronized in Java.

Answer: Volatile ensures visibility—writes to a volatile variable are visible to all readers—and prevents certain compiler/CPU optimizations. However, it doesn’t provide atomicity. For example, a volatile counter can be safely read, but increments (read-modify-write) aren’t atomic. Synchronized, on the other hand, provides both visibility and atomicity by acquiring an exclusive lock. Synchronized is more expensive (requires lock acquisition and release) but safer for compound operations. Use volatile for simple flags or status values; use synchronized for protecting critical sections with multiple operations.

Q3: What is a happens-before relationship, and why is it important?

Answer: A happens-before relationship is a guarantee provided by the Java Memory Model that defines when memory changes made in one thread are visible to another. Without happens-before guarantees, the JVM’s optimizer could reorder instructions in ways that break multithreaded code. Key happens-before relationships include: (1) releasing a lock happens-before acquiring it, (2) writing to a volatile field happens-before reading it, (3) calling Thread.start() happens-before actions in the started thread. These guarantees ensure that synchronization primitives actually work. For example, if Thread A writes to a field, releases a lock, and Thread B acquires the same lock, the happens-before relationship guarantees that B sees A’s write. This is why synchronization is not just about atomicity but about visibility.

Q4: How would you detect and prevent deadlocks?

Answer: Deadlocks occur when two or more threads wait for each other’s locks in a circular dependency. Prevention strategies include: (1) always acquire locks in the same order across your codebase, (2) use timeouts with locks (e.g., tryLock(timeout)), (3) avoid nested locks if possible, (4) use higher-level constructs like ReentrantLock with fairness guarantees. To detect deadlocks, you can use thread dumps (jstack command) to identify circular wait conditions. In production, monitoring tools can detect prolonged lock contention. The best approach is prevention through careful design—ensure lock ordering is consistent everywhere.

Q5: Why is ConcurrentHashMap better than Collections.synchronizedMap()?

Answer: Collections.synchronizedMap() wraps a HashMap with a synchronized lock applied to every operation. This means all threads compete for a single lock, creating a bottleneck. ConcurrentHashMap uses bucket-level locking (lock striping), where different buckets have separate locks. This allows multiple threads to write to different buckets concurrently. ConcurrentHashMap also provides atomic compound operations like putIfAbsent() and compute(), which are not possible with synchronized maps without risking race conditions. For read-heavy workloads, ConcurrentHashMap is significantly faster because reads often don’t require locking at all (via volatile fields and proper visibility).

Q6: What are the benefits of using ExecutorService over manually creating threads?

Answer: Creating a new Thread for each task is expensive and wasteful. ExecutorService maintains a pool of reusable threads, reducing thread creation overhead and enabling better resource management. Key benefits: (1) thread pooling reuses threads for multiple tasks, (2) you can control the pool size, preventing thread explosion, (3) ExecutorService provides Future objects for tracking task results, (4) you can submit multiple tasks and wait for completion with methods like invokeAll(), (5) graceful shutdown is easier with shutdown() and awaitTermination(). In production systems, always use ExecutorService instead of manually managing threads. Common patterns: fixed pool for CPU-bound tasks, cached pool for I/O-bound tasks, and single-threaded executor for tasks requiring sequential processing.

Q7: Explain the difference between CountDownLatch and CyclicBarrier.

Answer: Both synchronization utilities coordinate multiple threads, but with different semantics. CountDownLatch is initialized with a count and threads decrement it via countDown(); when the count reaches zero, waiting threads are released. It’s one-way: once the latch is open, it can’t be reset. CyclicBarrier, on the other hand, waits for a fixed number of threads to reach a barrier point via await(). Once all threads arrive, they’re released and the barrier resets for reuse. Use CountDownLatch for scenarios like “wait for N tasks to complete” or “wait for the application to initialize.” Use CyclicBarrier for recurring synchronization points, like workers processing batches of data with barriers between batches.

Q8: What is StampedLock and when should you use it?

Answer: StampedLock is a newer synchronization mechanism optimized for high-concurrency, read-heavy scenarios. Unlike ReadWriteLock, StampedLock supports optimistic reads—a thread can read data without acquiring a lock, then validate (via a stamp) that no write occurred during the read. If validation fails, the thread retries with a pessimistic read lock. This dramatically reduces lock contention for reads. However, StampedLock is not reentrant, and the read logic must be idempotent to handle validation failures. Use StampedLock when you have very high read concurrency (e.g., caches with millions of reads per second). For typical applications, ReentrantReadWriteLock is simpler and sufficient. StampedLock requires careful implementation to avoid bugs.

Q9: How would you design a thread-safe lazy singleton?

Answer: The best approach is using an eager static initializer (thread-safe by class loading semantics) or a private static inner class holder (lazy initialization with thread safety). Here’s the inner class approach: create a private static inner class that holds the singleton instance. The class is only loaded when first accessed, and class loading is thread-safe. This avoids the complexity and potential bugs of double-checked locking. Never use simple synchronized methods for lazy singletons in production—they’re inefficient due to lock contention on every access.

Q10: What causes memory leaks in multithreaded applications, and how do you prevent them?

Answer: Common causes: (1) ThreadLocal variables not being removed, causing objects to persist until the thread dies, (2) holding references in shared collections that are never cleaned up, (3) threads not terminating, keeping their stack memory alive, (4) circular references in task queues of ExecutorService. Prevention: (1) always remove ThreadLocal values in finally blocks, (2) use try-with-resources for AutoCloseable resources, (3) ensure threads can terminate gracefully with shutdown mechanisms, (4) use bounded queues in ExecutorService to prevent unbounded growth, (5) monitor heap usage with profilers during load testing. In one Infosys project I reviewed, a developer forgot to remove ThreadLocal values, causing a 10GB memory leak over 24 hours in a high-traffic system.

Practical Code Examples

Example 1: Thread-Safe Cache with Write-Through Strategy


import java.util.concurrent.locks.ReentrantReadWriteLock;
import java.util.*;

public class ThreadSafeCache<K, V> {
    private final Map<K, V> cache = new HashMap<>();
    private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
    
    public V get(K key) {
        lock.readLock().lock();
        try {
            return cache.get(key);
        } finally {
            lock.readLock().unlock();
        }
    }
    
    public void put(K key, V value) {
        lock.writeLock().lock();
        try {
            cache.put(key, value);
            // Optionally persist to database here (write-through)
        } finally {
            lock.writeLock().unlock();
        }
    }
    
    public void invalidate(K key) {
        lock.writeLock().lock();
        try {
            cache.remove(key);
        } finally {
            lock.writeLock().unlock();
        }
    }
    
    public void clear() {
        lock.writeLock().lock();
        try {
            cache.clear();
        } finally {
            lock.writeLock().unlock();
        }
    }
}

This cache uses ReadWriteLock for high read concurrency. In a scenario with 100 read threads and 1 writer, reads proceed concurrently without blocking each other. Writes block readers briefly, ensuring consistency. Use this pattern when reads far outweigh writes.

Example 2: Custom Thread Pool with Task Rejection Handling


import java.util.concurrent.*;

public class TaskProcessor {
    private final ExecutorService executor;
    
    public TaskProcessor(int threadCount, int queueSize) {
        BlockingQueue queue = new LinkedBlockingQueue<>(queueSize);
        
        executor = new ThreadPoolExecutor(
            threadCount,              // core pool size
            threadCount * 2,          // max pool size
            60, TimeUnit.SECONDS,     // keep-alive time
            queue,
            new ThreadFactory() {
                private final AtomicInteger count = new AtomicInteger(0);
                @Override
                public Thread newThread(Runnable r) {
                    Thread t = new Thread(r, "TaskProcessor-" + count.incrementAndGet());
                    t.setDaemon(false);
                    return t;
                }
            },
            new ThreadPoolExecutor.CallerRunsPolicy() // rejection handler
        );
    }
    
    public Future<?> submitTask(Runnable task) {
        return executor.submit(task);
    }
    
    public void shutdown() throws InterruptedException {
        executor.shutdown();
        if (!executor.awaitTermination(10, TimeUnit.SECONDS)) {
            executor.shutdownNow();
        }
    }
}

This demonstrates thread pool configuration with a custom ThreadFactory for named threads (useful for debugging) and CallerRunsPolicy for graceful degradation when the queue is full. This pattern is used in production systems at companies like TCS and Infosys.

Example 3: Parallel Task Execution with CyclicBarrier


import java.util.concurrent.*;

public class ParallelDataProcessor {
    public static void main(String[] args) throws InterruptedException {
        int numWorkers = 4;
        int batchSize = 1000;
        
        CyclicBarrier barrier = new CyclicBarrier(numWorkers, () -> {
            System.out.println("Batch processing complete. Starting new batch...");
        });
        
        ExecutorService executor = Executors.newFixedThreadPool(numWorkers);
        
        for (int batch = 0; batch < 3; batch++) {
            for (int i = 0; i < numWorkers; i++) {
                final int workerId = i;
                executor.submit(() -> processBatch(workerId, batchSize, barrier));
            }
        }
        
        executor.shutdown();
        executor.awaitTermination(1, TimeUnit.MINUTES);
    }
    
    private static void processBatch(int workerId, int batchSize, CyclicBarrier barrier) {
        try {
            System.out.println("Worker " + workerId + " processing batch of " + batchSize + " items");
            Thread.sleep(100 + new Random().nextInt(200)); // Simulate work
            barrier.await(); // Wait for all workers to finish this batch
        } catch (InterruptedException | BrokenBarrierException e) {
            Thread.currentThread().interrupt();
        }
    }
}

This demonstrates synchronized batch processing. Each worker processes data independently, then waits at the barrier for all workers to finish before proceeding to the next batch. The barrier action (lambda) runs once when all threads arrive, useful for cleanup or logging between batches.

Common Mistakes to Avoid

  • Not synchronizing compound operations: Checking if a key exists and then putting it in a HashMap looks safe but has a race condition. Use putIfAbsent() or synchronize the entire operation. Many developers make this mistake even at experienced levels.
  • Mixing synchronized and unsynchronized access: If some code synchronizes on a variable and other code doesn’t, you have a race condition. Ensure all access to shared state is coordinated. This is subtle because some accesses might be “read-only” but still need synchronization for visibility.
  • Holding locks during I/O operations: A thread holding a lock while performing network calls or database queries blocks other threads unnecessarily. Release locks before I/O. For example, don’t hold a cache lock while fetching from a database.
  • Using volatile for objects instead of references: volatile guarantees visibility of the reference, not the object’s fields. If you need visibility of nested fields, you still need synchronization. This is a subtle mistake that causes hard-to-debug visibility issues.
  • Ignoring InterruptedException: Swallowing InterruptedException (catching and ignoring) breaks thread interruption semantics. Always re-interrupt or propagate the exception. Many thread pools rely on interruption for graceful shutdown.
  • Creating too many threads: Thread creation is expensive, and the OS has limits. Always use ExecutorService with appropriately sized pools. Creating a new thread for each request is a common anti-pattern in high-traffic systems.
  • Not handling RejectedExecutionException: When an ExecutorService’s queue is full and no more threads can be created, submission fails. Handle this gracefully, don’t let tasks silently fail.
  • Assuming single-threaded behavior in multithreaded context: Code that works perfectly in single-threaded mode often breaks under concurrency due to visibility and ordering issues. Always test with high concurrency.

Best Practices for Interviews

  1. Always explain the problem before the solution. Describe the race condition, visibility issue, or atomicity problem you’re addressing. Interviewers value clear reasoning over quick answers.
  2. Discuss trade-offs explicitly. “This approach is simpler but has lower concurrency; that approach is more complex but scales better.” Show that you understand the cost-benefit analysis.
  3. Reference the Java Memory Model when relevant. Mentioning happens-before relationships and visibility guarantees demonstrates deep understanding. Don’t just say “synchronize it”; explain why.
  4. Provide working code examples. Write clean, compilable code that you can run on the spot if asked. Include proper exception handling and resource cleanup.
  5. Discuss real-world scenarios. Relate your answer to systems at scale. “In a cache with millions of reads per second, this approach would have lock contention issues…” shows practical thinking.
  6. Consider performance implications.** Discuss Big-O complexity for lock acquisition, context switch overhead, and memory barriers. Interviewers expect architectural thinking from senior candidates.
  7. Handle edge cases. What if a thread is interrupted? What if an exception occurs while holding a lock? What if the number of threads exceeds the thread pool size? Show defensive coding.
  8. Know when to use concurrency utilities. Don’t implement your own solutions when ConcurrentHashMap, ExecutorService, or synchronization utilities exist. But know how to implement from scratch if needed.
  9. Ask clarifying questions.** “Is this read-heavy or write-heavy?” “What’s the expected thread count?” “Are there latency requirements?” These questions show that you design for specific constraints.
  10. Demonstrate debugging skills. Explain how you’d identify a deadlock (jstack command), profile lock contention (JProfiler, YourKit), or analyze JVM logs for synchronization issues.

Final Recommendations

Multithreading interviews at tier-1 companies expect you to go beyond textbook answers. You should be able to:

  • Design systems that handle high concurrency without bottlenecks
  • Reason about visibility, atomicity, and ordering from first principles
  • Implement synchronization patterns correctly without introducing bugs
  • Choose the right tool for the job (synchronized vs. ReentrantLock vs. StampedLock, etc.)
  • Explain why certain approaches fail under specific conditions

The questions I’ve covered are representative of what you’ll encounter at TCS, Infosys, Wipro, and EPAM. However, company-specific follow-ups matter too. At microservices companies, expect questions about distributed locks (Redis, Zookeeper). At data processing companies, expect questions about batch processing and backpressure. Tailor your preparation accordingly.

Practice implementing these patterns from scratch, not just reading about them. The ability to code a thread-safe cache, implement a producer-consumer pattern, or design a task executor under interview pressure separates candidates who get offers from those who don’t.

If you want detailed mock interviews with expert feedback, check out our premium interview preparation platform where you can practice with real questions from companies like TCS, Infosys, and Wipro.

Frequently Asked Questions

Q: Is double-checked locking still recommended in 2026?

Double-checked locking works with volatile but introduces subtle bugs if misunderstood. Modern alternatives are preferable: use lazy static initialization with a class holder (thread-safe by design), use Supplier with memoization, or use frameworks like Spring that handle lazy initialization. If you must implement it, stick to the volatile pattern shown earlier, but generally avoid it in new code.

Q: What’s the difference between notify() and notifyAll()?

notify() wakes a single waiting thread (arbitrarily chosen), while notifyAll() wakes all waiting threads. In most cases, use notifyAll() because it’s safer—you don’t know which thread should wake up, so let all of them wake and compete for the lock. notify() is a micro-optimization that rarely matters and introduces subtle bugs. Exception: when you’re certain only one thread is waiting, notify() is marginally more efficient.

Q: How do I handle timeouts in multithreaded code?

Use ReentrantLock.tryLock(timeout) instead of synchronized, since synchronized doesn’t support timeouts. Use BlockingQueue.poll(timeout) instead of take(). Use ExecutorService.awaitTermination(timeout) for graceful shutdown. Always define timeouts conservatively—too short and you get spurious failures; too long and your system hangs. In production, use monitoring to tune timeouts based on actual latency.

Q: Can I use synchronized with multiple objects for finer-grained locking?

Yes, but it’s error-prone. Synchronizing on different objects for different data allows concurrent access but risks deadlocks if threads acquire locks in different orders. ReentrantLock or ReadWriteLock provide clearer intent and better debugging. For complex locking patterns, avoid synchronized and use explicit locks.

Q: What’s the performance cost of synchronization?

In uncontended scenarios (single thread or no competition), modern JVMs optimize synchronized via biased locking—almost zero cost. Under contention, costs escalate: lock inflation, context switching, cache misses. Rule of thumb: 100-200 uncontended lock acquisitions per microsecond, but 1000x slower under contention. This is why lock striping and concurrent data structures matter.

Q: How do I test multithreaded code for race conditions?

Stress testing with high thread counts increases probability of detecting race conditions. Use tools like ThreadSanitizer (C++, limited for Java) or FindBugs to detect potential issues. Tools like jcstress from OpenJDK systematically test concurrent scenarios. Write tests with 1000+ threads accessing shared state, then check for inconsistencies. In interviews, explain your testing approach—it shows you think about correctness under concurrency.

Q: When should I use Semaphore instead of other synchronization mechanisms?

Semaphore allows a fixed number of threads to access a resource. Use it for resource pooling (e.g., “allow max 10 concurrent database connections”), rate limiting, or implementing producer-consumer patterns. If you need simple mutual exclusion, use Lock. If you need thread coordination, use CountDownLatch or CyclicBarrier. Semaphore is for controlling concurrent access to a limited resource.

Q: What’s the difference between Future and CompletableFuture?

Future represents a computation that will complete in the future; you can check if it’s done or wait for the result. It’s blocking—calling get() waits. CompletableFuture is non-blocking and composable; you can chain transformations, combine multiple futures, and handle exceptions declaratively. CompletableFuture is modern and preferred for async operations. Use CompletableFuture for microservices calling multiple APIs; use Future for simple thread pool results.

Q: How do I gracefully shutdown an ExecutorService?

Call shutdown() to stop accepting new tasks, then awaitTermination(timeout) to wait for running tasks. If timeout expires, call shutdownNow() to interrupt remaining tasks. Always use try-finally to ensure shutdown happens. Best practice: use ExecutorService in a try-with-resources block (if it’s AutoCloseable in your Java version) or create a wrapper that manages shutdown gracefully.

Q: What happens if an exception occurs in a thread?

By default, the exception is printed to stderr and the thread terminates. If other threads depend on this thread (e.g., via join()), they won’t know about the exception. Use UncaughtExceptionHandler to handle exceptions globally. In ExecutorService, exceptions in tasks are stored in the Future returned by submit(); retrieve them via Future.get(). Always think about exception propagation in multithreaded systems—unhandled exceptions can silently break your application.

 

Also read our Java basics interview questions.

Also read our Java advanced interview questions.

Also read our book a mock interview.

Quick Reference

Aspect Key Point
When to use Depends on access pattern and thread safety needs
Interview tip Always explain time/space complexity with a real example

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *