A Spring Boot 3.2 service on Java 21 turns on virtual threads. An hour into peak traffic, with Redis running a slow failover, the service stops responding entirely — no errors logged, just silence. The rate limiter's `tryAcquire` method is `synchronized` and calls Redis inside the block. Explain the failure mechanism, and say what changes about it on Java 24.
On Java 21 through 23, a virtual thread that blocks inside a synchronized block cannot unmount from its carrier — the JVM tracks the monitor it holds on the carrier's own stack, so releasing the carrier mid-hold isn't possible the way it is for an ordinary blocking call outside a lock. With a small, fixed number of carriers (one per CPU core, eight here) and a rate limiter that's synchronized around a Redis call, each request that entered tryAcquire while Redis was slow stayed mounted and pinned its carrier for the entire duration of that slow call. Once eight requests were simultaneously inside tryAcquire — the exact number of cores — every carrier was pinned, and nothing else in the entire application, including the requests that would have finished quickly and freed things up, could be scheduled onto any carrier at all; the whole service froze, not gracefully degraded, because there were no carriers left for anything. Java 24 (JEP 491) rewrote monitor handling so that a virtual thread blocking on or inside synchronized now unmounts like any other blocking call, which would have let the other 39,992 waiting virtual threads keep making progress on the remaining carriers instead of the service going fully silent.