Chaturmind
LearnDSASystem DesignInterview PrepDevOpsEngineering GrowthBlog
Start learning
Chaturmind

Structured learning paths for engineers who want to go deep. Written by practitioners.

Learn

  • Java
  • DSA
  • System Design
  • Spring Boot
  • AI / ML
  • DevOps
  • Engineering Growth
  • Java Interview Prep

Company

  • Blog
  • Contact

Legal

  • Privacy Policy
  • Terms of Service

© 2026 Chaturmind. All rights reserved.

Built for engineers who want to go deep.


← Java Interview Prep: 5–8 Years

Advanced Core Java

  • Advanced OOP & Design Scenarios — Interview Questions
  • Advanced Concurrency, Collections & Memory — Interview Questions
  • Modern Java Language Features & Annotations — Interview Questions
  • Class Loading, Reflection, Serialization & Idioms — Interview Questions

Java Design Patterns in Depth

  • Design Patterns Overview & Singleton — Interview Questions
  • Factory & Abstract Factory Patterns — Interview Questions
  • Builder & Prototype Patterns — Interview Questions
  • Adapter & Bridge Patterns — Interview Questions
  • Composite & Decorator Patterns — Interview Questions
  • Facade & Proxy Patterns — Interview Questions
  • Chain of Responsibility & Observer Patterns — Interview Questions
  • Strategy & Template Method Patterns — Interview Questions
  • Command Pattern — Interview Questions

Advanced Spring Boot

  • Logging, Configuration & Actuator (Advanced) — Interview Questions
  • Transactions, Multiple Datasources & Query Tuning — Interview Questions
  • Validation & REST API Design (Advanced) — Interview Questions
  • Reactive, Async & Scheduling in Spring Boot — Interview Questions
  • Deployment, High Availability, Scaling & Caching — Interview Questions
Chaturmind
← Java Interview Prep: 5–8 Years

Advanced Core Java

  • Advanced OOP & Design Scenarios — Interview Questions
  • Advanced Concurrency, Collections & Memory — Interview Questions
  • Modern Java Language Features & Annotations — Interview Questions
  • Class Loading, Reflection, Serialization & Idioms — Interview Questions

Java Design Patterns in Depth

  • Design Patterns Overview & Singleton — Interview Questions
  • Factory & Abstract Factory Patterns — Interview Questions
  • Builder & Prototype Patterns — Interview Questions
  • Adapter & Bridge Patterns — Interview Questions
  • Composite & Decorator Patterns — Interview Questions
  • Facade & Proxy Patterns — Interview Questions
  • Chain of Responsibility & Observer Patterns — Interview Questions
  • Strategy & Template Method Patterns — Interview Questions
  • Command Pattern — Interview Questions

Advanced Spring Boot

  • Logging, Configuration & Actuator (Advanced) — Interview Questions
  • Transactions, Multiple Datasources & Query Tuning — Interview Questions
  • Validation & REST API Design (Advanced) — Interview Questions
  • Reactive, Async & Scheduling in Spring Boot — Interview Questions
  • Deployment, High Availability, Scaling & Caching — Interview Questions
HomeLearnJava Interview PrepJava Interview Prep: 5–8 YearsAdvanced Core Java
✓ FreeAdvanced· 11 min read

Advanced Concurrency, Collections & Memory — Interview Questions

Protecting critical sections, collections for a high-frequency trading book, ConcurrentModificationException causes and fixes, ReentrantLock vs synchronized, building safe concurrent task processing, immutability for threads, atomicity without synchronized, investigating OutOfMemoryError, reference types, Metaspace vs PermGen, a correct producer–consumer, and executor interruption.

Published September 25, 2026


How to use this lesson

These questions blend concurrency correctness with performance under load. Answer with the right primitive, then explain why it's correct: which happens-before edge it creates, what contention it causes, and how you'd verify it (tests, JFR, thread dumps).

Q1. How do you manage access to a critical section that touches a shared resource?

Short answer: First try to eliminate the sharing: confinement, immutability, or partitioning by key. If you can't:

  • Protect all accesses to the shared state with one lock. Use synchronized, or a ReentrantLock when you need timeouts, interruptibility or fairness.
  • Keep the critical section small: no I/O, and no calls to unknown code while holding the lock.
  • Use a Semaphore when N concurrent users are allowed (for example, a limited connection pool).
  • Use a ReadWriteLock or StampedLock for read-heavy data.
private final ReentrantLock lock = new ReentrantLock();
boolean reserveSeat(String seat) throws InterruptedException {
    if (!lock.tryLock(100, TimeUnit.MILLISECONDS)) throw new BusyException();   // bounded waiting
    try {
        if (booked.contains(seat)) return false;
        booked.add(seat);
        return true;
    } finally {
        lock.unlock();
    }
}

Key points to cover:

  • In a cluster, a JVM lock protects only one instance. Shared resources such as a seat or a stock count need database-level protection (conditional updates, SELECT … FOR UPDATE, optimistic locking) or a distributed lock.

Learn it in depth → Synchronized and Locks

Q2. A high-performance trading application frequently updates and sorts prices. Which collections would you use?

Short answer: You need sorted, concurrent access to price levels. A plain TreeMap/TreeSet keeps order, with O(log n) operations, but it isn't thread-safe. The options:

  • ConcurrentSkipListMap<BigDecimal or long, PriceLevel>: sorted, concurrent, lock-free reads, O(log n). Its firstKey(), ceilingKey() and headMap() give best-bid/best-ask and depth queries.
  • The single-writer principle: one thread owns the order book, and receives updates through a queue (a ring buffer, as in the LMAX Disruptor). Then the structures don't need to be concurrent, and latency is predictable.
  • Avoid boxing and GC churn: prices as long ticks (fixed-point), primitive collections (Eclipse Collections, fastutil or Agrona), and preallocated objects.
NavigableMap<Long, Level> bids = new ConcurrentSkipListMap<>(Comparator.reverseOrder());   // best bid first
NavigableMap<Long, Level> asks = new ConcurrentSkipListMap<>();                            // best ask first
long bestBid = bids.firstKey();
long spread = asks.firstKey() - bestBid;

Common trap: "a TreeMap, because it's sorted". It's correct only if a single thread touches it. And re-sorting a list on every tick is O(n log n) per update.

Q3. What causes ConcurrentModificationException, and how do you prevent it?

Short answer: Fail-fast iterators track a modCount. If the collection is structurally modified during iteration, other than through the iterator itself, the next next() throws. The cause is often a single thread (removing inside a for-each loop), not concurrency.

Prevention:

  • Use iterator.remove(), removeIf, or list.replaceAll.
  • Collect the changes and apply them after the loop.
  • Iterate over a copy.
  • For genuine multi-threaded access, use concurrent collections (ConcurrentHashMap, or CopyOnWriteArrayList for read-mostly lists). Their weakly consistent iterators never throw.
orders.removeIf(Order::isCancelled);                          // instead of remove() inside for-each

Key points to cover:

  • The exception is best-effort. Unsynchronised concurrent modification can also corrupt a collection silently, so don't rely on the exception as your concurrency check.

Q4. What is a ReentrantLock, and how does it differ from synchronized?

Short answer: Both are reentrant mutual-exclusion locks with the same memory semantics. ReentrantLock adds:

  • tryLock (with a timeout);
  • lockInterruptibly;
  • fairness;
  • multiple Conditions;
  • introspection (getQueueLength, isHeldByCurrentThread).

The cost is that you must unlock manually in finally. synchronized is simpler, can't leak, and is well optimised.

Key points to cover:

  • Before JDK 24, virtual threads pinned their carrier thread inside synchronized blocks, so ReentrantLock was preferred for blocking code on virtual threads. JDK 24 (JEP 491) removed most of that pinning.

Learn it in depth → Synchronization, Locks & Deadlocks

Q5. You need to process tasks concurrently. Which Java constructs ensure efficient and safe execution?

Short answer:

  • Execution: an ExecutorService, either a bounded ThreadPoolExecutor (sized for CPU-bound or I/O-bound work, with a bounded queue and a rejection policy) or virtual threads (newVirtualThreadPerTaskExecutor) for blocking I/O.
  • Composition: CompletableFuture (supplyAsync, thenCombine, allOf, orTimeout), or structured concurrency (StructuredTaskScope, a preview API), for fan-out/fan-in with cancellation.
  • Shared state: avoid it. Otherwise use concurrent collections, atomics, or locks.
  • Coordination: CountDownLatch, Semaphore (to limit concurrency towards a downstream service), Phaser.
  • Lifecycle: graceful shutdown, cooperative interruption, and metrics on the pools.
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
    List<Future<Quote>> quotes = suppliers.stream()
            .map(s -> executor.submit(() -> s.quote(request)))
            .toList();
    // collect results with timeouts; cancel the rest on the first failure if needed
}

Learn it in depth → ExecutorService

Q6. Why is immutability so valuable in multi-threaded applications?

Short answer: An immutable object has no state changes, so there are no races, no need for locks, and no torn reads. With final fields and safe construction, the JMM guarantees every thread sees it fully initialised. You can share it freely, cache it, and use it as a map key. To "update" shared immutable state, publish a new instance atomically (volatile or AtomicReference), which is the copy-on-write style.

Key points to cover:

  • The trade-off is allocation: modern GCs handle short-lived objects well, but hot loops creating many copies can matter.
  • Persistent data structures (Vavr, PCollections) reduce copying cost.

Q7. How do you ensure atomicity without synchronized?

Short answer:

  • Atomic classes (AtomicInteger, AtomicLong, AtomicReference), using CAS (compare-and-swap) loops: incrementAndGet, compareAndSet, updateAndGet, accumulateAndGet.
  • LongAdder/LongAccumulator, for highly contended counters.
  • ConcurrentHashMap.compute and merge, for per-key atomic updates.
  • VarHandle, for low-level atomic field access.
  • Explicit Locks, which are still locks, just without the keyword.
private final AtomicReference<Balance> balance = new AtomicReference<>(Balance.ZERO);
boolean withdraw(long amount) {
    while (true) {
        Balance cur = balance.get();
        if (cur.amount() < amount) return false;
        if (balance.compareAndSet(cur, cur.minus(amount))) return true;   // retry if another thread won the race
    }
}

Key points to cover:

  • CAS gives atomicity for one variable. Invariants spanning several variables need a lock, or a single immutable object swapped atomically, as above.

Q8. The logs show OutOfMemoryError. How do you investigate?

Short answer:

  1. Read the exact message:
    • Java heap space, or GC overhead limit exceeded: a heap problem.
    • Metaspace: class metadata, or class-loader leaks.
    • unable to create native thread: too many threads, or OS limits.
    • Direct buffer memory: NIO/Netty off-heap memory.
    • The container OOM-killer (exit code 137): the total footprint exceeds the container limit.
  2. Get evidence: a heap dump (have -XX:+HeapDumpOnOutOfMemoryError configured in advance), GC logs, JFR recordings, and memory metrics over time.
  3. Analyse the dump in Eclipse MAT: the dominator tree, leak suspects, and paths to GC roots. Is it a leak (steadily growing retained objects) or a spike (one huge request, an unbounded query or cache)?
  4. Fix the cause: bound the caches and queues, stream large results, fix the leak, then right-size the heap and container limits (-XX:MaxRAMPercentage).
  5. Verify under a load test, and add alerts on post-GC old-generation usage.

Learn it in depth → Memory Leaks in Java

Q9. What are strong, weak, soft and phantom references, and what role do they play in GC?

Short answer:

  • Strong: normal references. The object can't be collected while one exists.
  • Soft (SoftReference): cleared at the GC's discretion, before an OutOfMemoryError. They were historically used for memory-sensitive caches.
  • Weak (WeakReference): cleared at the next GC, once no strong or soft references remain. Used by WeakHashMap keys and canonicalising maps.
  • Phantom (PhantomReference): get() always returns null. The reference is enqueued after the object becomes unreachable, so you can run post-mortem cleanup of native resources. This is the mechanism behind java.lang.ref.Cleaner, the replacement for finalize().

Key points to cover:

  • Prefer explicit size- or time-bounded caches (Caffeine) to soft-reference caches. The behaviour of soft references under memory pressure is unpredictable, and can cause GC thrashing.

Q10. What is Metaspace, and how does it differ from PermGen?

Short answer: PermGen (up to Java 7) was a fixed-size region of the Java heap holding class metadata and, before Java 7, interned strings. Too many classes led to OutOfMemoryError: PermGen space. Metaspace (Java 8+) stores class metadata in native memory, and grows dynamically, with -XX:MaxMetaspaceSize as an optional cap. Interned strings and static fields live on the regular heap.

Key points to cover:

  • Metaspace can still leak, through class-loader leaks. Cap it in containers, so a leak fails clearly instead of consuming all the memory.

Q11. Write the producer–consumer problem with wait and notify.

Short answer: Use a bounded buffer guarded by one monitor:

  • wait in a loop while the condition is false;
  • use notifyAll(), because producers and consumers share the same wait set;
  • never sleep while holding the lock;
  • handle interruption properly.
public final class BoundedBuffer<T> {
    private final Deque<T> items = new ArrayDeque<>();
    private final int capacity;
    public BoundedBuffer(int capacity) { this.capacity = capacity; }

    public synchronized void put(T item) throws InterruptedException {
        while (items.size() == capacity) wait();       // loop: spurious wakeups, and competing producers
        items.addLast(item);
        notifyAll();                                    // wake consumers (notify() could wake another producer)
    }

    public synchronized T take() throws InterruptedException {
        while (items.isEmpty()) wait();
        T item = items.removeFirst();
        notifyAll();                                    // wake producers waiting for space
        return item;
    }
}

// usage: producers call buffer.put(x); consumers call buffer.take(); simulate work OUTSIDE the lock:
// T job = buffer.take(); process(job);

Common trap: the widespread version calls Thread.sleep() inside the synchronized block, so the other side can never make progress during the sleep. It also uses a single notify() with two different wait conditions, which can wake the wrong kind of thread, and stall. In production, use a BlockingQueue (ArrayBlockingQueue), or a ReentrantLock with two Conditions (notFull, notEmpty).

Learn it in depth → Producer-Consumer Class Design

Q12. How does the Executor framework handle task interruption, and what are the best practices?

Short answer: Executors interrupt worker threads on shutdownNow(), and on Future.cancel(true). Interruption is cooperative: the task must notice it (blocking calls throw InterruptedException, or it checks Thread.currentThread().isInterrupted()), then stop. Best practices:

  • Check the interrupt status in long CPU loops.
  • Never swallow InterruptedException. Either propagate it, or restore the flag with Thread.currentThread().interrupt().
  • Clean up in finally.
  • Make tasks idempotent, so cancelled work can be retried safely.
  • Use timeouts (future.get(timeout), orTimeout), and cancel on timeout.
  • Shut executors down gracefully (shutdown → awaitTermination → shutdownNow).

Key points to cover:

  • Plain blocking socket I/O (InputStream.read) doesn't respond to interruption. Use socket timeouts, or NIO interruptible channels. CompletableFuture.cancel(true) doesn't interrupt the running task, it only completes the future.

Follow-up questions this topic invites — and their answers

Q: What is false sharing? A: Two threads write different variables that sit on the same CPU cache line, so the line bounces between cores, and performance drops. Mitigate it with padding (@Contended in JDK internals), or by separating hot fields. LongAdder is designed with this in mind.

Q: What is StampedLock's optimistic read? A: You read without locking, then validate the stamp. If a writer intervened, you retry, or fall back to a read lock. It's very fast for read-mostly data, but it isn't reentrant, and it needs careful use.

Q: How do you choose the pool size for I/O-bound tasks? A: A rough formula is threads ≈ cores × (1 + wait time / compute time), validated by load testing, or use virtual threads, and limit concurrency towards each downstream service with semaphores.

Q: What does -XX:+ExitOnOutOfMemoryError do, and why use it? A: It terminates the JVM on the first OOM. That's better than limping on in an undefined state. The orchestrator restarts the container, and the heap dump (if configured) is captured first.

Previous

Advanced OOP & Design Scenarios — Interview Questions

Next

Modern Java Language Features & Annotations — Interview Questions

AI Tutor

Lesson: Advanced Concurrency, Collections & Memory — Interview Questions

Quick actions

AI responses can be inaccurate. Verify critical information.