# 1. Why InheritableThreadLocal behaves differently with virtual threads [Next: Async + virtual threads →](02-async-virtual-threads.md) `Demo1PlainThreadLocal.java` has no Spring in it at all. It exists to settle one question before Spring Security enters the picture: does `InheritableThreadLocal` actually behave differently once the thread on the other end is virtual? ## The three cases Every `Thread` copies the creating thread's `InheritableThreadLocal` values **once, at construction time**. That single sentence explains everything Spring Security's concurrency support has ever had to work around: - A **fresh platform `Thread`** picks up whatever was set on the thread that created it. Fine. - A **pooled platform thread** was constructed once, long ago, by the pool's internal thread factory. Every task submitted to it later runs on that same physical thread, so it keeps whatever `InheritableThreadLocal` value existed *when the pool created the worker*, not what the submitting thread had at submission time. `docs/output/demo1.txt` shows this directly: task 2 on a reused pool worker still reports `request-B`, not `request-C`, even though the caller updated the value in between. - A **virtual thread** is, in this respect, identical to the fresh-platform-thread case. `Executors.newVirtualThreadPerTaskExecutor()` and `Thread.ofVirtual().start(...)` both construct a brand new `Thread` object per task -- virtual threads are never pooled or reused the way platform worker threads are. So the "stale value from a reused thread" failure mode that made `SecurityContextHolder.MODE_INHERITABLETHREADLOCAL` dangerous with `ThreadPoolTaskExecutor` simply does not exist for virtual threads. ## Why this matters for the rest of the repo Spring Security's docs (and the original version of the blog post this repo supports) warn against `MODE_INHERITABLETHREADLOCAL` because of the pooled-thread case above. That warning is correct for `ThreadPoolTaskExecutor`. It stops being the relevant risk once `spring.threads.virtual.enabled=true` swaps the executor for a `SimpleAsyncTaskExecutor` backed by virtual threads -- there is no pool left to go stale. [Chapter 2](02-async-virtual-threads.md) verifies that directly against `SecurityContextHolder`. Run it yourself: `scripts/run-all.sh`, or just `demo1` from the output already captured in [`docs/output/demo1.txt`](output/demo1.txt).