skip to content

Compare MODE_THREADLOCAL and MODE_INHERITABLETHREADLOCAL. When would you switch strategy, and what's the risk?

level: middleimportance: must knowfreq 65%

answer

  1. THREADLOCAL = isolated per thread (default)
  2. INHERITABLETHREADLOCAL = child inherits at creation
  3. GLOBAL = one JVM-wide context (desktop only)
  4. pool = inherits ONCE -> stale/leak
  5. pools -> DelegatingSecurityContext* not inheritance

basics

~20 s

MODE_THREADLOCAL (default) stores the SecurityContext per-thread, so child threads don't see it. MODE_INHERITABLETHREADLOCAL uses an InheritableThreadLocal so a thread you spawn inherits the parent's context. The risk: with pooled threads inheritance is unreliable and can leak stale identities.

solid answer

~40 s

SecurityContextHolder supports three strategies, chosen via SecurityContextHolder.setStrategyName(...) or the spring.security.strategy property. MODE_THREADLOCAL is the default: each thread has its own SecurityContext, so a manually spawned child thread sees an empty context. MODE_INHERITABLETHREADLOCAL backs it with an InheritableThreadLocal, so a child thread created from a request thread automatically inherits a copy of the parent's context at creation time. MODE_GLOBAL uses one static context for the whole JVM — only for standalone/desktop apps. You'd switch to INHERITABLETHREADLOCAL for simple @Async or new Thread() work that needs the caller's identity. The big risk is thread pools: inheritance happens at thread *creation*, so a pooled worker keeps whatever context it had when first created and won't refresh per task — leading to stale or leaked identities. For pools, prefer explicit propagation (DelegatingSecurityContext*) over inheritance.

code

java · 17 lines
java
// Set the strategy once, very early at startup
@Configuration
public class SecurityStrategyConfig {
    @PostConstruct
    void init() {
        SecurityContextHolder.setStrategyName(
            SecurityContextHolder.MODE_INHERITABLETHREADLOCAL);
    }
}

// Safer for POOLS: wrap the executor so context propagates per-task
@Bean
public Executor taskExecutor() {
    ThreadPoolTaskExecutor delegate = new ThreadPoolTaskExecutor();
    delegate.initialize();
    return new DelegatingSecurityContextExecutor(delegate);
}

go deeper

for a junior

Just know the default is THREADLOCAL and that spawned threads don't automatically see the context.

for a middle

Name all three modes, explain inheritance semantics, and the timing constraint for setting the strategy.

for a senior

Articulate the thread-pool inheritance bug and why explicit DelegatingSecurityContext* propagation is the correct fix.

for a principal

Weigh strategy choice against the concurrency model (pools vs fresh threads vs virtual threads) and set org-wide guidance to avoid identity leaks.

`SecurityContextHolder` delegates storage to a `SecurityContextHolderStrategy`, and there are three built-in strategies selected by mode: **1. `MODE_THREADLOCAL` (default).** Uses `ThreadLocalSecurityContextHolderStrategy`, backed by a plain `java.lang.ThreadLocal`. Each thread has an isolated context. This is ideal for servlet web apps because each request runs on its own thread and the value is naturally isolated and cleared at request end. Consequence: if your request handler does `new Thread(...).start()` or hands work to a thread pool, that other thread's holder is **empty** — the context does not follow. **2. `MODE_INHERITABLETHREADLOCAL`.** Uses `InheritableThreadLocalSecurityContextHolderStrategy`, backed by `java.lang.InheritableThreadLocal`. When a *new* thread is created, the JVM copies inheritable-thread-local values from the creating (parent) thread into the child. So a child thread spawned during a request will see the parent's `SecurityContext`. This makes naive multithreading 'just work' for identity. **3. `MODE_GLOBAL`.** Uses `GlobalSecurityContextHolderStrategy` — a single static context shared by every thread in the JVM. Only appropriate for standalone/Swing-style single-user applications; never for a server. **How to set the mode:** - Programmatically: `SecurityContextHolder.setStrategyName(SecurityContextHolder.MODE_INHERITABLETHREADLOCAL);` — must run very early (e.g., a `@PostConstruct` or static init) before any context is stored. - Property: set `spring.security.strategy` (Boot) or the system property `spring.security.strategy`. **The thread-pool gotcha (the whole reason this is a senior/middle topic):** `InheritableThreadLocal` copies the value at **thread creation time only**. Thread pools (like the executor behind `@Async` or a servlet container) create their worker threads once and reuse them for many tasks. So a worker inherits whatever context existed when *it* was born — possibly none, or possibly the identity of the very first request that triggered its creation — and then keeps serving other users' tasks with that stale context. This is both a **correctness bug** (wrong user) and a potential **security leak** (one user acting as another). Therefore INHERITABLETHREADLOCAL is safe only for threads you create fresh per unit of work, not for pooled executors. **The recommended alternative for pools:** don't rely on inheritance. Use Spring's explicit propagation wrappers: `DelegatingSecurityContextRunnable`, `DelegatingSecurityContextCallable`, `DelegatingSecurityContextExecutor`, or `DelegatingSecurityContextAsyncTaskExecutor`. These capture the context at *submit* time and install it on the worker for the duration of that task, then restore it — per task, correctly, even on a pool. For `@Async`, register a `DelegatingSecurityContextAsyncTaskExecutor` or the `SecurityContext` `@Async` support. There is also `SecurityContextHolder.MODE_THREADLOCAL` combined with these delegates as the standard pattern. **Other subtleties:** - The strategy is JVM-global — you cannot mix modes per request. - Changing strategy at runtime after contexts are already stored is unsafe; set it once at startup. - With Java 21 virtual threads, per-task threads are cheap, but the same 'inheritance copies once' semantics apply; explicit propagation remains the robust choice.

  • Why does MODE_INHERITABLETHREADLOCAL misbehave with @Async's default executor?
    @Async uses a thread pool. InheritableThreadLocal copies the context only when a worker thread is first created, not on each task submission — so pooled workers carry a stale (or wrong-user) context. Use DelegatingSecurityContextAsyncTaskExecutor to propagate per task instead.
  • How do you change the strategy, and what's the constraint on timing?
    Call SecurityContextHolder.setStrategyName(...) or set the spring.security.strategy property. It must be done at startup before any SecurityContext is stored, because the strategy is JVM-global and switching after the fact orphans already-stored contexts.

saying these in an interview costs you the question

  • Recommending MODE_INHERITABLETHREADLOCAL for thread-pool / @Async work
  • Claiming InheritableThreadLocal re-copies the context on every task
  • Suggesting MODE_GLOBAL for a web server
  • Thinking you can set the strategy per request or safely switch it at runtime

context