Back to Blog
Java

Java ThreadLocalRandom: Contention-Free Random Numbers

java threadlocalrandom: Learn how ThreadLocalRandom avoids seed contention in concurrent Java code, with usage examples for current(), bounded generation, and primitiv...

ThreadLocalRandomJava ConcurrencyRandom Number Generationjava.util.concurrentParallel Streams
Illustration of ThreadLocalRandom providing per-thread random number generators in parallel lanes without shared state contention

java threadlocalrandom requires a clear understanding of the core syntax, runtime behavior, and practical implementation patterns demonstrated in the examples below.

Why Shared Random Instances Cause Contention

java.util.Random derives its values from a 48-bit seed updated with a linear congruential formula. When several threads share a single Random instance, every call to nextInt() or nextLong() must update that seed atomically. The update uses a compare-and-set loop, and when many threads compete for the same instance, those CAS attempts fail and retry repeatedly. The result is wasted CPU cycles and degraded throughput under load.

ThreadLocalRandom avoids this by giving each thread its own seed. You obtain the generator through ThreadLocalRandom.current(), which returns the instance bound to the calling thread. Because no seed state is shared between threads, there is no contention point and no atomic update to coordinate.

import java.util.concurrent.ThreadLocalRandom; public class RandomExample { public static void main(String[] args) { int value = ThreadLocalRandom.current().nextInt(); System.out.println(value); } }

Basic Generation with ThreadLocalRandom.current()

The entry point is always current(). You do not construct a ThreadLocalRandom with new; the constructor is not part of the public API. Calling current() from different threads returns instances that each use the calling thread's own seed, so the same code works correctly in single-threaded and concurrent settings.

int randomInt = ThreadLocalRandom.current().nextInt(); long randomLong = ThreadLocalRandom.current().nextLong(); double randomDouble = ThreadLocalRandom.current().nextDouble(); boolean randomBoolean = ThreadLocalRandom.current().nextBoolean();

The returned values cover the full range of each primitive type, including negative integers and longs. For most application code you will want bounded values instead, which the next section covers.

Bounded and Unbounded Generation

The class provides overloads that accept an exclusive upper bound or an inclusive lower bound plus an exclusive upper bound.

// 1 inclusive to 7 exclusive int dice = ThreadLocalRandom.current().nextInt(1, 7); // 0 inclusive to 101 exclusive int percent = ThreadLocalRandom.current().nextInt(101); // 1.0 inclusive to 10.0 exclusive double delay = ThreadLocalRandom.current().nextDouble(1.0, 10.0); // -50 inclusive to 50 exclusive long offset = ThreadLocalRandom.current().nextLong(-50, 50);

The single-argument nextInt(bound) form requires a positive bound. The two-argument forms require origin < bound. Passing invalid bounds throws IllegalArgumentException, so validate input before calling these methods when the bounds come from user data or configuration.

Stream Methods for Bulk Generation

When you need many values at once, ThreadLocalRandom offers ints(), longs(), and doubles() methods that return primitive streams. These are useful for filling arrays, generating test data, or feeding parallel stream operations.

int[] values = ThreadLocalRandom.current() .ints(10, 1, 100) .toArray();

The first argument is the stream size, followed by the origin and bound. Without a size argument the stream is infinite, so you must apply limit() or another short-circuit operation. The stream methods are particularly valuable in parallel streams because each worker thread draws from its own seed, avoiding the contention a shared Random would introduce.

ThreadLocalRandom vs Random vs Math.random()

AspectThreadLocalRandomjava.util.RandomMath.random()
ConcurrencyPer-thread seed, no contentionShared seed, CAS contentionDelegates to shared Random
Seed controlNot supportedSupportedNot supported
Bounded overloadsYesYesOnly via arithmetic
Typical useConcurrent code, parallel streamsSingle-threaded codeSimple one-off values

Math.random() internally delegates to a shared Random instance, so it carries the same contention cost when called from many threads. It also only returns double values, so producing an integer requires additional arithmetic. For any code that runs inside a thread pool or parallel stream, ThreadLocalRandom is the more appropriate choice.

Performance and Contention Behavior

The performance benefit of ThreadLocalRandom comes from removing the shared atomic seed update, not from a faster algorithm. Each thread keeps its own seed in thread-local storage, so calls never block and never retry a failed CAS. Under low concurrency the difference is small; under high concurrency, where many threads call the same Random instance simultaneously, the retry overhead becomes measurable.

The class is also the default generator used by the parallel stream machinery, which is why parallelStream() operations do not contend on a shared Random. If you are generating random values inside a parallelStream() pipeline, using ThreadLocalRandom.current() directly keeps the same per-thread behavior.

Limitations and Edge Cases

ThreadLocalRandom is not cryptographically secure. Use SecureRandom when the generated values protect secrets, tokens, or passwords.

You cannot seed the generator. setSeed() throws UnsupportedOperationException, so you cannot reproduce a sequence for testing. If you need deterministic output, use a single Random instance with a fixed seed in single-threaded code.

The current() call is cheap but not free. In a tight loop, store the instance in a local variable rather than calling current() on every iteration.

ThreadLocalRandom random = ThreadLocalRandom.current(); for (int i = 0; i < 1000; i++) { int value = random.nextInt(1, 100); }

Bound validation matters: nextInt(0) throws IllegalArgumentException, as does nextInt(5, 5). Always confirm that the origin is strictly less than the bound before passing values that originate from external input.

java threadlocalrandom: Practical Usage and Code Examples | RYUSLOG DEV