Back to Blog
Java

Java Random vs ThreadLocalRandom Comparison

java random vs threadlocalrandom: Compare java.util.Random and ThreadLocalRandom for concurrent Java applications. Understand contention, thread safety, and when to us...

JavaRandomThreadLocalRandomConcurrencyPerformance
Illustration comparing java.util.Random and ThreadLocalRandom for concurrent Java applications

When generating random numbers in Java, developers often reach for java.util.Random without considering its behavior in multi-threaded contexts. In concurrent applications, this can introduce avoidable contention and degrade throughput. ThreadLocalRandom offers a per-thread alternative that sidesteps the shared-state bottleneck. This article compares java random vs threadlocalrandom to help you decide which one fits your use case.

The Contention Problem with java.util.Random

java.util.Random uses a 48-bit seed that is updated using a linear congruential generator (LCG). To produce the next random value, the instance must read and write this seed. The class is thread-safe, but it achieves that safety by using an AtomicLong internally. Every call to next() performs a compare-and-set (CAS) operation on the atomic seed. When multiple threads call the same Random instance concurrently, they compete to update the seed. The CAS loop retries when contention is high, which can lead to significant CPU cache traffic and reduced throughput.

Consider a simple example where several worker threads share a single Random instance:

import java.util.Random; public class SharedRandomExample { private static final Random RANDOM = new Random(); public static void main(String[] args) { Runnable task = () -> { for (int i = 0; i < 1000; i++) { int value = RANDOM.nextInt(100); // process value } }; for (int t = 0; t < 8; t++) { new Thread(task).start(); } } }

Each thread calls RANDOM.nextInt() and contends for the same atomic seed. The more threads you add, the more contention you get. This is not a correctness problem—the values remain random—but it is a performance problem in high-throughput scenarios.

How ThreadLocalRandom Works

ThreadLocalRandom was introduced in Java 7 to address this contention. Instead of sharing a single seed across threads, each thread gets its own seed and generator state. The class is a singleton that delegates to a per-thread seed stored in the Thread object itself. Because each thread updates only its own seed, there is no CAS contention. The implementation is also faster than Random for single-threaded use because it avoids the atomic overhead entirely.

To use ThreadLocalRandom, you call its static current() method to obtain the generator for the current thread:

import java.util.concurrent.ThreadLocalRandom; public class ThreadLocalRandomExample { public static void main(String[] args) { Runnable task = () -> { ThreadLocalRandom random = ThreadLocalRandom.current(); for (int i = 0; i < 1000; i++) { int value = random.nextInt(100); // process value } }; for (int t = 0; t < 8; t++) { new Thread(task).start(); } } }

Notice that current() is called inside the task, not before. This is important: ThreadLocalRandom.current() returns a generator bound to the calling thread. If you call it once and share the result across threads, the behavior is undefined and can lead to incorrect results. The API is designed to be used in a thread-local manner.

API Differences: Random vs ThreadLocalRandom

Both classes provide methods like nextInt(), nextLong(), nextDouble(), and nextBoolean(). The signatures are mostly identical. The key difference lies in how you obtain an instance.

Random uses a constructor:

Random random = new Random();

You can optionally pass a seed to the constructor to reproduce a sequence.

ThreadLocalRandom has no public constructor. You must call ThreadLocalRandom.current() to get the instance for the current thread. This method returns a singleton per thread, so repeated calls within the same thread return the same object.

Another difference is the handling of the seed. Random allows you to set the seed via setSeed(long). ThreadLocalRandom does not support this; calling setSeed throws UnsupportedOperationException. This is intentional—each thread's seed is managed internally and cannot be manually overridden.

Featurejava.util.RandomThreadLocalRandom
Instance creationnew Random()ThreadLocalRandom.current()
Thread safetyThread-safe (atomic seed)Not thread-safe by design; use per-thread
Seed controlCan set seedCannot set seed
ContentionHigh under multi-threaded useNone (per-thread state)
PerformanceSlower in concurrent scenariosFaster in concurrent and single-threaded
Java versionSince 1.0Since Java 7

Performance and Concurrency Considerations

The primary performance difference stems from the atomic seed update in Random versus the thread-local state in ThreadLocalRandom. In a single-threaded environment, ThreadLocalRandom is also slightly faster because it avoids the atomic operation entirely. However, the difference is usually small for low-frequency calls.

The real win appears in multi-threaded applications. When many threads call Random concurrently, the CAS operations cause cache-line ping-ponging. Each thread's write invalidates the cache line holding the seed, forcing other threads to reload it. This can become a bottleneck that scales poorly with thread count. ThreadLocalRandom eliminates this shared cache line, allowing each thread to operate on its own seed without interference.

There is no need to synchronize access to a ThreadLocalRandom instance because it is not shared. However, you must ensure that each thread uses its own instance. The typical pattern is to call current() at the beginning of a task or method and reuse it throughout that thread's execution.

For parallel streams, ThreadLocalRandom is the recommended choice. Java's parallel stream framework executes tasks on a common fork-join pool, and each worker thread can call ThreadLocalRandom.current() without contention. Using a shared Random inside a parallel stream would reintroduce the same contention problem.

Choosing Between Random and ThreadLocalRandom

Use ThreadLocalRandom in almost all new code, especially if there is any chance the code will run in a multi-threaded context. It is the safer default because it avoids contention without requiring you to manage separate Random instances per thread.

Use Random when you need reproducibility through a manually set seed. For example, if you are simulating a process and need to reproduce the exact sequence of random numbers across runs, Random with a fixed seed is appropriate. ThreadLocalRandom does not allow seed setting, so it cannot provide this guarantee.

Use Random when you need to share a single random source across threads and you are certain the call rate is low enough that contention is negligible. For instance, a configuration value read occasionally does not need the performance of ThreadLocalRandom. But even then, ThreadLocalRandom is not harmful—it just requires a call to current().

In short, the decision comes down to two factors: whether you need seed control and whether the generator will be used concurrently. If you need seed control, use Random. Otherwise, prefer ThreadLocalRandom.

Common Pitfalls and Misconceptions

One common mistake is storing a ThreadLocalRandom instance in a static field and sharing it across threads. Because ThreadLocalRandom is not thread-safe, this can lead to incorrect random values or exceptions. The correct usage is to call current() inside each thread's execution context.

Another misconception is that ThreadLocalRandom is a replacement for Random in every scenario. While it is faster and contention-free, it does not support seeding. If you rely on a reproducible sequence, Random is the only option.

A related pitfall is using new Random() inside a method that is called frequently. Each call creates a new instance, which is wasteful. If you need a single-threaded random source, reuse a single Random instance or use ThreadLocalRandom.current() if you are already in a thread context.

Finally, be aware that ThreadLocalRandom is not available before Java 7. If you are maintaining legacy code on Java 6 or earlier, you must stick with Random or implement your own thread-local wrapper. In modern Java, ThreadLocalRandom is the recommended choice for most random number generation needs.

java random vs threadlocalrandom: Practical Usage and Code E | RYUSLOG DEV