Yes, Java ThreadLocalRandom is Safe to Use with Virtual Threads

Because Java 21 virtual threads are very cheap — per JEP Cafe, they are about 1,000 times faster to launch than a platform thread, and use about 1,000 times less memory than a platform thread — they should never be pooled. As a result, ThreadLocal variables are unlikely to be useful in applications that use virtual threads for concurrent processing. If each task lives in its own dedicated thread, then each call to ThreadLocal ends up returning a new value, and it ends up being a factory instead of a pool! So using ThreadLocal in virtual threads is an anti-pattern. Developers should use another technique for pooling objects, such as structured concurrency, instead.

But what about ThreadLocalRandom? Is this dedicated class for providing sources of randomness to threads safe to use with virtual threads?

The short answer is: Yes, ThreadLocalRandom is safe to use with virtual threads. ThreadLocalRandom is tightly integrated with the Thread class to ensure that it remains efficient, even for virtual threads. For more information, keep reading.

Understanding ThreadLocal

Internally, ThreadLocal works by maintaining a mapping from a Thread to its corresponding value. When code asks a ThreadLocal for a value, it follows this process:1

  1. Look up the current thread.
  2. Check internal map for the current thread’s value.
  3. If found, then return the value. Otherwise, go to step 4.
  4. Create a new value, and store it as the current thread’s value in the internal map.
  5. Return the new value.

If an application creates millions of threads — which is exactly how virtual threads are designed to be used — and each one calls into ThreadLocal and then does its work and dies, then the ThreadLocal will accumulate millions of dead objects in its internal map. ThreadLocal‘s internal map uses weak references, so these dead objects won’t contribute to an OOM, but they also won’t be collected until the JVM starts to experience memory pressure, so before they are collected, the dead objects will trigger extra GC cycles, possibly including full GCs, which are preferably (and typically) avoided.

Hence, using ThreadLocal with virtual threads is an anti-pattern.

Understanding ThreadLocalRandom

ThreadLocalRandom, on the other hand, uses fields directly embedded in the Thread object. When the user asks ThreadLocalRandom for an instance of Random, it initializes with the current thread — platform or virtual — and initializes these fields. These fields are all primitives, so there is no risk of object leaks in any case. The returned Random instance then uses these embedded fields to generate data.

Hence, using ThreadLocalRandom with virtual threads is perfectly safe. Indeed, it appears tailor-made to work with them.

Conclusions

As a result of this design, every Thread pays a (small) overhead of about 20 bytes each to ensure that a simple, easy-to-use source of randomess is available, whether they use it or not. And because of its simplicity, it works well for any type of Thread, be it platform, virtual, or any other Thread types Java may gain in the future.

That seems a worthy tradeoff.

Footnotes

  1. This process is oversimplified. The actual class is properly synchronized to ensure that it is thread-safe, allows users to set and remove objects explicitly, and so on.