Back to Blog
Java

Java Random Class: Usage, Seeding, and Thread Safety

java random class: Learn how to use java.util.Random correctly: constructors, methods, seeding, thread safety, and when to choose ThreadLocalRandom or SecureRandom.

java.util.RandomThreadLocalRandomSecureRandomrandom number generationJava concurrency
Illustration of Java's Random class showing a seed input and multiple output streams, with a thread-safety indicator and alternatives.

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

The java.util.Random class is the standard way to generate pseudo-random values in Java. It provides a fast, deterministic generator that is suitable for simulations, testing, and any non-security-sensitive randomness. This article covers how to use it correctly, how seeding works, and how to avoid common concurrency pitfalls.

What java.util.Random Provides

At its core, Random is a linear congruential generator (LCG). It produces a sequence of numbers that appear random but are entirely determined by an initial seed. The simplest usage is to create an instance and call one of its next methods:

import java.util.Random; Random random = new Random(); int value = random.nextInt();

The no-argument constructor uses the system clock as the seed, so each instance typically produces a different sequence. If you need a specific sequence for testing, you can pass a long seed to the constructor.

How Random Generates Values

The seed is a 48-bit value that is updated internally after each call. The algorithm is defined in the Java specification, so the sequence is reproducible across JVM implementations as long as the same seed and method calls are used. This determinism is valuable when you need to reproduce a failure or verify an algorithm.

The generator is not cryptographically secure. The internal state can be inferred after observing enough output, and the sequence is predictable. For security-sensitive contexts, use SecureRandom instead.

Core Methods for Common Use Cases

Random offers several methods for different data types. The most frequently used are:

  • nextInt() – returns a uniformly distributed int over the full range.
  • nextInt(int bound) – returns an int between 0 (inclusive) and bound (exclusive).
  • nextLong() – returns a uniformly distributed long.
  • nextDouble() – returns a double between 0.0 (inclusive) and 1.0 (exclusive).
  • nextFloat() – similar to nextDouble() but for float.
  • nextBoolean() – returns true or false with equal probability.
  • nextBytes(byte[]) – fills an array with random bytes.

Here is an example that uses several methods:

Random random = new Random(); int die = random.nextInt(6) + 1; // 1 to 6 long large = random.nextLong(); double ratio = random.nextDouble(); boolean flag = random.nextBoolean();

Note that nextInt(bound) requires bound to be positive. If you need a value in a specific range like 5 to 10, you can compute it as random.nextInt(6) + 5. On Java 17 and later, Random includes overloads like nextInt(int origin, int bound) that handle this directly, but the manual approach works on all versions.

Controlling the Seed for Reproducible Tests

Seeding is the mechanism that makes generated sequences reproducible. Two Random instances created with the same seed and called in the same order produce identical results. This is essential for unit tests where you need deterministic input data.

Random random1 = new Random(42L); Random random2 = new Random(42L); System.out.println(random1.nextInt()); // -1170105035 System.out.println(random2.nextInt()); // -1170105035

You can also reseed an existing instance using setSeed(long). This resets the internal state and starts a new sequence. Be aware that reseeding a Random that is shared across threads can cause unexpected behavior because the state change is not atomic.

Thread Safety and Concurrency

Random instances are thread-safe in the sense that individual method calls are atomic. However, they use an internal atomic seed update, which creates contention when many threads call methods concurrently. This contention can become a performance bottleneck in high-throughput applications.

A better choice for multi-threaded code is ThreadLocalRandom. Each thread gets its own generator, eliminating contention and improving scalability. It also has a more efficient seeding strategy for concurrent use.

import java.util.concurrent.ThreadLocalRandom; int value = ThreadLocalRandom.current().nextInt(1, 7); // 1 to 6

ThreadLocalRandom does not support explicit seeding. If you need reproducibility in a concurrent test, you can still use a single Random instance, but you must manage synchronization yourself, or use a thread-local wrapper that seeds each thread independently.

Choosing Between Random, ThreadLocalRandom, and SecureRandom

The right generator depends on your requirements. The table below summarizes the key differences:

CriterionRandomThreadLocalRandomSecureRandom
Deterministic with seedYesNoNo
Thread-safeYes, but contendedYes, per-threadYes
Performance in high concurrencyModerateHighLow
Cryptographic securityNoNoYes
Best fitSingle-threaded or low contentionHigh-throughput concurrent codeSecurity tokens, passwords, keys

Use Random when you need reproducibility and are working in a single-threaded context. Use ThreadLocalRandom for most concurrent code where reproducibility is not required. Use SecureRandom for any value that must be unpredictable to an attacker.

Generating Numbers Within a Range

A common task is generating an integer in a specific range. The standard approach is to use nextInt(bound) and shift the result. For example, to get a value between min and max inclusive:

int randomInRange = random.nextInt(max - min + 1) + min;

This works because nextInt(bound) returns 0 to bound-1, and adding min shifts the range. Be careful with overflow when max - min + 1 exceeds Integer.MAX_VALUE; for large ranges, use nextLong() and scale accordingly.

If you are on Java 17 or later, you can use the built-in overload:

int randomInRange = random.nextInt(min, max + 1);

The origin is inclusive and the bound is exclusive, so max + 1 is needed for an inclusive upper bound.

Common Mistakes and How to Avoid Them

One frequent error is creating a new Random instance on every call. This is wasteful and can produce identical sequences if the constructor is called within the same millisecond, especially in tight loops. Instead, create one instance and reuse it.

Another mistake is using Random for security-sensitive data like session IDs or password reset tokens. The generator is predictable, and an attacker who observes a few outputs can recover the seed and predict future values. Always use SecureRandom for such cases.

Finally, be careful with Random in multi-threaded code. Even though it is thread-safe, the shared atomic state can cause performance degradation. Prefer ThreadLocalRandom unless you need deterministic output across threads.

Understanding the java random class and its alternatives lets you choose the right tool for each scenario. The key is to match the generator's properties—determinism, thread safety, and security—to your application's requirements.

java random class: Practical Usage and Code Examples | RYUSLOG DEV