JUnit Jupiter's @Timeout annotation accepts a threadMode of SAME_THREAD or SEPARATE_THREAD. Explain how each one executes the annotated method and the risks of choosing either.
answer
- INFERRED → thread.mode.default → SAME_THREAD
- same thread = interrupt only, cooperative
- separate thread = preemptive but leaks the thread
- ThreadLocal: transactions, SecurityContext, MDC
- JUnit 4 @Test(timeout) always used a separate thread
basics
~20 sSAME_THREAD runs the method on the calling thread and merely interrupts it at the deadline, so uninterruptible code keeps running. SEPARATE_THREAD runs it on another thread and fails immediately at the deadline, abandoning that thread and losing thread-bound state such as transactions.
solid answer
~50 s`threadMode` decides who executes the annotated method. **SAME_THREAD** (the effective default) executes the test on the normal calling thread; a watchdog thread interrupts it when the budget expires. Because the thread is unchanged, everything thread-affine still works — Spring's transactional test support, `ThreadLocal` security contexts, `MDC`. The cost is that interruption is cooperative: a busy loop, a classic blocking socket read, or a native call ignores it, so the failure is only recorded when the method finally returns and the build is not actually unblocked. **SEPARATE_THREAD** runs the body on a different thread while the calling thread waits with a bounded join. At the deadline the test fails at once — genuinely preemptive — but the runaway thread is abandoned and keeps running, and any `ThreadLocal`-bound state does not carry over, which breaks transaction rollback and similar mechanisms. The default is `INFERRED`, resolved from the `junit.jupiter.execution.timeout.thread.mode.default` configuration parameter, which is `SAME_THREAD`.
code
java · 14 linesimport java.util.concurrent.TimeUnit;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.Timeout;
import org.junit.jupiter.api.Timeout.ThreadMode;
class LegacyClientTest {
@Test
@Timeout(value = 2, unit = TimeUnit.SECONDS,
threadMode = ThreadMode.SEPARATE_THREAD)
void nativeCallThatIgnoresInterrupts() {
legacyClient.blockingRead();
}
}go deeper
It is enough to say one mode runs the test on the normal thread and the other on a separate thread, and that the default is the same thread.
Explain interrupt-versus-abandon and name at least one thread-bound mechanism (transaction, ThreadLocal) that separate-thread mode breaks.
Own the trade explicitly: cooperative but safe versus preemptive but leaky, plus the diagnosis that a firing timeout indicates uninterruptible blocking to fix in the code under test.
Talk about it as suite policy — default safe mode, narrow opt-in for known-uninterruptible code, and a process-level watchdog as the real guarantee, since no in-JVM mechanism can stop arbitrary code.
## Why there is a choice at all Stopping a running piece of Java code from outside is not something the JVM offers safely. `Thread.stop` is unsafe and removed; the only cooperative mechanism is interruption, and the only preemptive one is to stop *waiting* for the work and walk away, leaving it running. JUnit exposes exactly that trade as `Timeout.ThreadMode`. ## SAME_THREAD The annotated method is invoked on the same thread that the engine would have used anyway. A separate watchdog thread tracks the deadline; when it expires it calls `interrupt()` on the test thread and records a `TimeoutException` failure. What this preserves is *thread affinity*. A great deal of Java infrastructure binds state to the current thread through `ThreadLocal`: Spring's `TransactionSynchronizationManager` (which is how `@Transactional` tests roll back), Spring Security's `SecurityContextHolder`, SLF4J's `MDC`, many JPA `EntityManager` bindings, and most request-scoped test doubles. If the test body suddenly ran on a different thread, none of that would be visible, so the test would fail or silently leak. This is precisely why Jupiter made same-thread the default — JUnit 4's `@Test(timeout = ...)` always used a separate thread and broke such setups in confusing ways. What this sacrifices is enforcement. Interruption only takes effect if the code is in an interruptible state — `Thread.sleep`, `Object.wait`, `BlockingQueue.take`, `Future.get`, `Lock.lockInterruptibly`, interruptible NIO channels — or explicitly polls `Thread.currentThread().isInterrupted()`. Code that swallows `InterruptedException` in a retry loop, spins on the CPU, blocks on a legacy `Socket` `InputStream`, or sits in a native call will keep going. The test is still ultimately reported as timed out, but not until the method returns, which may be never. In that case the run hangs exactly as it would have without the annotation. ## SEPARATE_THREAD The body is submitted to another thread; the calling thread waits with a bounded timeout. When the budget expires the wait ends, a `TimeoutException` failure is recorded, and execution moves on immediately. This behaves like Jupiter's preemptive timeout assertion helper and, historically, like JUnit 4's timeout attribute. The wins are real: a genuinely stuck test no longer stalls the suite, and you get a prompt, deterministic failure. The costs are equally real: 1. **Thread leak.** The abandoned thread keeps running. If it holds a lock, an open transaction, or a connection from a pool, later tests can deadlock, see dirty data, or exhaust the pool. Under a long suite these leaks accumulate. 2. **Lost thread-bound state.** Anything installed on the calling thread is invisible to the worker, and anything the worker installs is invisible to teardown. Transactional tests may not roll back; security contexts appear empty; MDC-based log correlation breaks. 3. **Confusing diagnostics.** The stack trace of the failure describes the waiting thread, and the leaked thread continues to write log output long after its test was reported. ## Choosing and configuring The annotation's default value is `ThreadMode.INFERRED`, which defers to the `junit.jupiter.execution.timeout.thread.mode.default` configuration parameter; that parameter itself defaults to `SAME_THREAD`. So doing nothing gives you same-thread semantics, and you can flip the default suite-wide or override per annotation: - Keep **SAME_THREAD** for anything using Spring transactions, `ThreadLocal` context, or thread-confined fixtures, and for tests whose blocking points are interruptible (most sleeps, waits, and modern HTTP clients). - Use **SEPARATE_THREAD** deliberately for a small set of tests exercising code known to ignore interruption, where letting the suite proceed matters more than a leaked thread — and treat any firing of it as an incident, not a routine outcome. A subtlety with parallel execution: separate-thread timeouts add threads beyond the configured parallelism, so a suite that already saturates its executor can end up with more concurrency than expected, changing timing and increasing the chance the timeout fires at all. ## Interview framing The crisp version: same thread is safe but only advisory; separate thread is enforcing but unsafe. Nothing in Java lets you have both, so the annotation makes you pick, and the sensible default is the safe one plus a fix for the underlying hang.
- Why does a @Transactional Spring test misbehave when its timeout uses SEPARATE_THREAD?Spring binds the transaction and its EntityManager to the current thread through TransactionSynchronizationManager, which is ThreadLocal-based. Running the test body on a different thread means it does not see the transaction the test framework began, so writes may commit outside it or fail to roll back, and the teardown on the original thread rolls back an empty transaction. This exact class of breakage is why Jupiter chose same-thread as the default.
- If SAME_THREAD cannot stop uninterruptible code, how do you keep such a test from hanging CI forever?You need a layer above the test. Options are a hard job-level or process-level watchdog that kills the JVM after a total budget, forking so a wedged JVM does not take the whole run with it, and selectively marking the known-uninterruptible tests with SEPARATE_THREAD so the suite proceeds. The durable fix is in the code under test: make the blocking call bounded by its own read timeout so it becomes interruptible or self-limiting.
Same-thread is knocking on the door and asking the occupant to leave; separate-thread is locking the door from outside and declaring the room empty while someone is still inside.
saying these in an interview costs you the question
- Believing SAME_THREAD forcibly kills the test at the deadline rather than interrupting it.
- Believing SEPARATE_THREAD stops the runaway work — it only stops waiting for it; the thread leaks.
- Not knowing why the default is same-thread, i.e. missing the ThreadLocal/transaction affinity argument.
- Assuming Jupiter's default matches JUnit 4's @Test(timeout=...), which always spawned a thread.
- Recommending SEPARATE_THREAD globally as a general hardening measure.