Java Backend Interview Q&A← All stacks

Concurrency & Multithreading — Interview Q&A Matrix

Parent stack: Core Java Runtime Technology: Concurrency & Multithreading Levels: Senior · Architect Questions: 4

Each entry contains a Question, the Expected Depth, an Ideal Answer, and Red Flags.

Senior

Q1 — ThreadLocal, thread pools, and leaks

Question: Why can ThreadLocal cause memory leaks in a servlet or thread-pool environment, and how do you use it safely?

Expected Depth:

Ideal Answer:

java
private static final ThreadLocal<UserContext> CTX = new ThreadLocal<>();

try {
    CTX.set(context);
    handle(request);
} finally {
    CTX.remove();
}
java
@Component
class ContextFilter extends OncePerRequestFilter {
    @Override
    protected void doFilterInternal(
            HttpServletRequest request,
            HttpServletResponse response,
            FilterChain chain) throws IOException, ServletException {
        try {
            CTX.set(UserContext.from(request));
            chain.doFilter(request, response);
        } finally {
            CTX.remove();
        }
    }
}
java
private static final ScopedValue<UserContext> CTX = ScopedValue.newInstance();

ScopedValue.where(CTX, UserContext.from(request))
        .run(() -> handle(request));

Red Flags:


Q2 — CompletableFuture composition and error handling

Question: How would you run two independent remote calls concurrently, combine their results, apply a timeout, and handle failures without waiting for each call in sequence?

Expected Depth:

Ideal Answer:

java
try (ExecutorService io = Executors.newVirtualThreadPerTaskExecutor()) {
    CompletableFuture<Price> price =
        CompletableFuture.supplyAsync(this::fetchPrice, io);
    CompletableFuture<Stock> stock =
        CompletableFuture.supplyAsync(this::fetchStock, io);

    Quote quote = price
        .thenCombine(stock, Quote::new)
        .orTimeout(500, TimeUnit.MILLISECONDS)
        .exceptionally(error -> Quote.fallback())
        .join();
}
java
CompletableFuture<Account> account = fetchUser(id)
    .thenCompose(user -> fetchAccount(user.accountId()))
    .thenApply(this::redactPii);
java
List<CompletableFuture<Quote>> futures = symbols.stream()
    .map(symbol -> CompletableFuture.supplyAsync(
        () -> fetchQuote(symbol), io))
    .toList();

CompletableFuture<List<Quote>> all = CompletableFuture
    .allOf(futures.toArray(CompletableFuture[]::new))
    .thenApply(ignored -> futures.stream()
        .map(CompletableFuture::join)
        .toList());
java
CompletableFuture<Quote> resilient = price
    .thenCombine(stock, Quote::new)
    .completeOnTimeout(Quote.stale(), 300, TimeUnit.MILLISECONDS)
    .handle((quote, error) ->
        error == null ? quote : Quote.fallback());

Red Flags:


Architect

Q3 — Virtual Threads (Project Loom): when and when not

Question: Your team wants to replace all thread pools with virtual threads to "make everything faster." How do you assess this proposal? Where do virtual threads help, and what are the risks?

Expected Depth:

Ideal Answer:

java
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
    requests.forEach(request ->
            executor.submit(() -> handle(request)));
} // close() waits for submitted tasks
java
synchronized (this) {
    updateLocalState();
}

callRemoteService(); // blocking work is outside the monitor
java
private final Semaphore dbPermits = new Semaphore(20);

void handle(Request request) throws InterruptedException {
    dbPermits.acquire();
    try {
        repository.save(request);
    } finally {
        dbPermits.release();
    }
}
bash
-Djdk.tracePinnedThreads=full

Red Flags:


Q4 — Choosing concurrency tools at scale

Question: Teams use synchronized, atomic classes, ConcurrentHashMap, StampedLock, and queues in different ways. What guidance would you provide for choosing the correct concurrency tool?

Expected Depth:

Ideal Answer:

java
private final AtomicLong balance = new AtomicLong();

balance.updateAndGet(current -> current + delta);
java
ConcurrentHashMap<String, Integer> counts = new ConcurrentHashMap<>();
counts.merge(key, 1, Integer::sum);
java
private final StampedLock lock = new StampedLock();
private double x;
private double y;

double distance() {
    long stamp = lock.tryOptimisticRead();
    double currentX = x;
    double currentY = y;

    if (!lock.validate(stamp)) {
        stamp = lock.readLock();
        try {
            currentX = x;
            currentY = y;
        } finally {
            lock.unlockRead(stamp);
        }
    }

    return Math.hypot(currentX, currentY);
}
java
Account first = a.id() < b.id() ? a : b;
Account second = a.id() < b.id() ? b : a;

first.lock.lock();
try {
    second.lock.lock();
    try {
        transfer(a, b, amount);
    } finally {
        second.lock.unlock();
    }
} finally {
    first.lock.unlock();
}
java
BlockingQueue<Task> queue = new ArrayBlockingQueue<>(1_000);

queue.put(task);          // waits when the queue is full
Task next = queue.take(); // waits when the queue is empty
java
try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
    var user = scope.fork(() -> loadUser(id));
    var permissions = scope.fork(() -> loadPermissions(id));

    scope.join().throwIfFailed();
    return new Profile(user.get(), permissions.get());
}

Red Flags:


Scoring Rubric

Level Pass bar
JuniorExplains basic concurrency concepts and uses standard APIs correctly.
SeniorExplains trade-offs, finds concurrency risks, and writes correct code.
ArchitectDefines clear platform guidance and measures behavior under realistic load.