Generate Random Numbers in Java With Random and RandomGenerator

Generate random numbers in Java with Math.random(), Random, ThreadLocalRandom, SplittableRandom, the Java 17 RandomGenerator and SecureRandom.

Decision tree that picks SecureRandom for security, Random with a seed for repeatable tests, SplittableRandom for parallel streams and ThreadLocalRandom for everything else

To generate random numbers in Java, we call a method such as nextInt(), nextDouble() or nextBoolean() on a random number generator, for example ThreadLocalRandom.current() in application code or SecureRandom for passwords and tokens. All generators except SecureRandom are predictable pseudorandom generators, which means a formula computes the numbers from a starting value called the seed.

We use random numbers for test data, games, shuffling, sampling, retry delays, A/B tests and one-time codes. Each use case puts different demands on the generator, such as repeatable output in tests, speed across threads or unpredictable values for security.

The following example generates a random number with each common API.

int dice = ThreadLocalRandom.current().nextInt(1, 7);        // 1 to 6
double fraction = Math.random();                             // 0.0 to just under 1.0
boolean coin = RandomGenerator.getDefault().nextBoolean();   // true or false
int repeatable = new Random(42).nextInt(100);                // 30, always 30 for seed 42
int code = new SecureRandom().nextInt(100_000, 1_000_000);   // 6-digit code

Notice that the bound in nextInt(1, 7) is excluded, so the dice never shows 7, and that a fixed seed returns the same value on every run. We compare the generators first and look at each one with its typical use, and finish with streams, the Java 17 algorithms and secure tokens.

1. Choosing How to Generate Random Numbers in Java

The JDK has six ways to get random numbers, and they differ in thread safety, seeding and security. Since Java 17, all generator classes implement the RandomGenerator interface from JEP 356, so they share the same nextInt(), nextLong(), nextDouble() and stream methods.

Decision tree that picks SecureRandom for security, Random with a seed for repeatable tests, SplittableRandom for parallel streams and ThreadLocalRandom for everything else
Security decides first, then repeatability, then parallel work; everything else goes to ThreadLocalRandom

The table sums up the differences. Thread-safe here means one instance can be shared by many threads without corrupting its state.

GeneratorSinceThread-safeSeed for repeatable outputSecureTypical use
Math.random()1.0YesNoNoA quick double in a demo or a script
Random1.0Yes, but threads compete for itYesNoRepeatable sequences in tests, older APIs
ThreadLocalRandom7One instance per threadNoNoApplication code, web servers, thread pools
SplittableRandom8No, split it per taskYesNoParallel streams and simulations
RandomGenerator.of(name)17NoYes, through the factoryNoA specific algorithm such as L64X128MixRandom
SecureRandom1.1YesSeeds only add to its entropyYesPasswords, tokens, keys, one-time codes

A pseudorandom generator is fast and good enough for games and tests, but someone who sees enough output can predict the next values. That is fine for a dice roll and a security hole for a password reset link.

2. Math.random() for a Quick Value

The method Math.random() returns a double from 0.0 (inclusive) to 1.0 (exclusive). On the first call, it creates one java.util.Random for the whole JVM, and every later call in every thread uses that instance.

double fraction = Math.random();                     // 0.0 to just under 1.0
int percent = (int) (Math.random() * 101);           // 0 to 100

For whole numbers, we have to scale and cast, and a wrong formula shifts the range. We prefer the nextInt(origin, bound) methods for integers, and our guide to random numbers in a range covers the formulas and their traps.

3. java.util.Random and Seeds

The class java.util.Random is the original generator. It has a method for each primitive type, and since Java 17 it also has the range methods of RandomGenerator, such as nextInt(origin, bound).

Random random = new Random();
int any = random.nextInt();                 // any int, negative values too
int below = random.nextInt(10);             // 0 to 9
int between = random.nextInt(5, 10);        // 5 to 9
double d = random.nextDouble();             // 0.0 to just under 1.0
boolean flag = random.nextBoolean();        // true or false
double gaussian = random.nextGaussian();    // normal distribution around 0.0

Two Random objects with the same seed return the same sequence of numbers, on every run and every JVM. The constructor without arguments picks a different seed each time, so we pass a seed only when we want the same numbers again.

Random first = new Random(42);
Random second = new Random(42);
int a = first.nextInt(100);                 // 30
int b = second.nextInt(100);                // 30
int next = first.nextInt(100);              // 63

A music app shuffles a playlist with Collections.shuffle(), and a unit test has to check the shuffled order. In production the app passes a new Random(), and the test passes new Random(7), so the expected order is fixed and the test never fails by chance.

List<String> playlist = new ArrayList<>(List.of("intro", "verse", "chorus", "bridge", "outro"));
Collections.shuffle(playlist, new Random(7));
List<String> order = playlist;              // [outro, bridge, intro, chorus, verse]

A Random instance is thread-safe, because it updates its seed with an atomic compare-and-set. Many threads that share one instance keep retrying that update, so the Javadoc recommends ThreadLocalRandom for multi-threaded code. We also reuse one instance instead of creating a new Random for every number.

4. Streams of Random Numbers

Every generator can return an IntStream, LongStream or DoubleStream of random values with ints(), longs() and doubles(). We pass the number of values and an optional range, or we limit an endless stream ourselves.

int[] rolls = new Random(42).ints(5, 1, 7).toArray();         // [3, 4, 1, 3, 1]
List<Integer> lotto = ThreadLocalRandom.current().ints(1, 50)
        .distinct()
        .limit(6)
        .sorted()
        .boxed()
        .toList();                                              // 6 different numbers from 1 to 49
double average = new Random(42).doubles(10_000).average().orElse(0);  // close to 0.5

The distinct() step is how we get random numbers without repetition from a stream. It works well when we need a few values from a large range. To pick all values of a small range in random order, shuffling a list is faster, because distinct() keeps drawing duplicates near the end.

5. ThreadLocalRandom in Multi-threaded Code

The class ThreadLocalRandom gives every thread its own generator, so threads never wait for each other. We always call ThreadLocalRandom.current() at the point of use, and never store the result in a field that another thread could read, because each instance belongs to one thread.

Say an online shop sends 10% of its checkout requests to a new checkout page. Hundreds of request threads call the method at the same time, which is the situation concurrent code has to handle without a shared lock.

static String checkoutVariant() {
    return ThreadLocalRandom.current().nextInt(100) < 10 ? "new-checkout" : "old-checkout";
}
String variant = checkoutVariant();                        // "new-checkout" in about 10% of calls
ThreadLocalRandom.current().setSeed(42);                   // UnsupportedOperationException

The second line shows the trade-off. A ThreadLocalRandom cannot be seeded, so code that needs repeatable values in tests takes a RandomGenerator parameter instead, as section 7 shows.

6. SplittableRandom for Parallel Work

The class SplittableRandom (Java 8) is not thread-safe. Instead of sharing one instance, we call split() to create an independent generator for each task, and the values of all parts together have the same statistical quality as one sequence. Parallel streams created from a SplittableRandom do this splitting for us.

SplittableRandom root = new SplittableRandom(42);
SplittableRandom child = root.split();
int fromRoot = root.nextInt(1, 7);                          // 1 to 6
int fromChild = child.nextInt(1, 7);                        // 1 to 6
long hits = new SplittableRandom(42).ints(1_000_000, 0, 100)
        .parallel()
        .filter(n -> n < 25)
        .count();                                           // close to 250000

We use SplittableRandom for simulations and fork/join tasks, where each subtask gets its own split generator. With a seed, the same split structure gives the same numbers again, which ThreadLocalRandom cannot do.

7. RandomGenerator and the Java 17 Algorithms

Java 17 added the RandomGenerator interface and a family of newer algorithms, mostly from the LXM family, which have longer periods and better statistical quality than the 48-bit formula of Random. The method RandomGenerator.getDefault() returns the recommended general-purpose algorithm, and RandomGenerator.of(name) returns a specific one.

RandomGenerator defaultGenerator = RandomGenerator.getDefault();
String defaultName = RandomGeneratorFactory.getDefault().name();          // "L32X64MixRandom"
RandomGenerator lxm = RandomGenerator.of("L64X128MixRandom");
int roll = lxm.nextInt(1, 7);                                              // 1 to 6
RandomGenerator seeded = RandomGeneratorFactory.of("Xoshiro256PlusPlus").create(42L);
long algorithms = RandomGeneratorFactory.all().count();                    // 13

JDK 25 lists 13 algorithms through RandomGeneratorFactory.all(), including Random, SecureRandom and SplittableRandom. We cover the factory and the jumpable and splittable algorithms in detail in Java random generators for concurrency.

The interface also makes code easier to test. A method that takes a RandomGenerator parameter gets ThreadLocalRandom.current() in production and a seeded generator in a test.

static int rollDice(RandomGenerator generator) {
    return generator.nextInt(1, 7);
}
int live = rollDice(ThreadLocalRandom.current());          // 1 to 6
int inTest = rollDice(new Random(42));                     // 3, always 3 for seed 42

8. SecureRandom for Tokens and Passwords

The class SecureRandom returns values that an attacker cannot predict, because it takes its seed material from the operating system. On Linux, the default algorithm on JDK 25 is NativePRNG, which reads from /dev/urandom. We use it for session IDs, password reset links, API keys, salts and one-time codes.

SecureRandom secure = new SecureRandom();
byte[] bytes = new byte[16];
secure.nextBytes(bytes);
String hexToken = HexFormat.of().formatHex(bytes);                          // 32 hex digits
String urlToken = Base64.getUrlEncoder().withoutPadding().encodeToString(bytes);  // 22 URL-safe characters
int otp = secure.nextInt(100_000, 1_000_000);                               // 6-digit code
String algorithm = secure.getAlgorithm();                                   // "NativePRNG"

We create the generator with new SecureRandom() and let the JDK pick the algorithm. When a specific algorithm is required, we call SecureRandom.getInstance(“DRBG”), which throws a checked NoSuchAlgorithmException if the platform lacks it. We never seed a SecureRandom with a fixed value to make it repeatable, and the setup options are covered in secure random number generation.

9. Java Random Number FAQs

Seeds, shared instances and repeated values are where a random number generator gives results that look wrong at first.

9.1. Is Math.random() thread-safe?

Yes. It uses one shared Random instance, which is thread-safe, but many threads calling it at the same time compete for that instance. In multi-threaded code we call ThreadLocalRandom.current() instead.

9.2. Is java.util.Random truly random?

No. It computes each value from the previous seed with a fixed formula, so the same seed always gives the same sequence. For values an attacker must not guess, we use SecureRandom.

9.3. How do we generate random numbers without repetition?

We either shuffle a list of all candidate values with Collections.shuffle() and take the first n, or draw from a stream with distinct().limit(n). Shuffling suits small ranges, and the stream suits a few values from a large range.

9.4. How do we get the same random numbers on every run?

We create the generator with a fixed seed, such as new Random(42) or RandomGeneratorFactory.of(“L64X128MixRandom”).create(42L), and call the same methods in the same order. ThreadLocalRandom and SecureRandom cannot do this.

9.5. Can Math.random() return 1.0?

No. It returns values from 0.0 up to, but excluding, 1.0, so (int) (Math.random() * 10) gives 0 to 9. For whole numbers in a range, ThreadLocalRandom.current().nextInt(1, 11) is shorter and returns 1 to 10.

10. Conclusion

For most code, ThreadLocalRandom.current() or RandomGenerator.getDefault() is the right generator. We use Random or a factory-created generator with a seed when tests need repeatable values, SplittableRandom for parallel streams, and SecureRandom whenever a guessed value would be a security problem.

Since Java 17, every generator shares the RandomGenerator methods, including nextInt(origin, bound) with an excluded upper bound. Passing a RandomGenerator into our methods keeps them testable without changing the production code.

11. References

Happy Learning !!

Source Code on Github

Leave a Comment

  1. Hi Lokesh,

    Your example is good. I would like to add UUID to this solution. This is because the chances of dupes is very minimal (almost never) and can be used in high TPS systems and even in a clustered system.

    public class UUIDTest {

    /**
    * @param args
    */
    public static void main(String[] args) {
    String uuid = UUID.randomUUID().toString();
    System.out.println(uuid);
    }
    }

Comments are closed.

About Us

HowToDoInJava provides tutorials and how-to guides on Java and related technologies.

It also shares the best practices, algorithms & solutions and frequently asked interview questions.