Part 9 of 14

Enhanced Pseudo-Random Number Generators (JEP 356): A Modern PRNG API

Finalized in Java 17 (JEP 356).

The Problem with Java’s Old PRNG API

Java’s random number generation before Java 17 had several problems:

1. java.util.Random has poor statistical properties. The default PRNG algorithm is a 48-bit linear congruential generator (LCG). It fails many statistical tests (spectral test, birthday test). Its 2^48 period is short — exhaustible in seconds on modern hardware.

2. No common interface. java.util.Random, java.util.concurrent.ThreadLocalRandom, java.util.SplittableRandom, and java.security.SecureRandom share no common supertype beyond Object. You cannot write code that works with any generator without casting.

3. ThreadLocalRandom cannot be passed around. It is tied to the current thread. Libraries that accept a Random parameter cannot use ThreadLocalRandom.

4. No algorithm selection. You cannot choose a different algorithm without writing your own implementation or using a third-party library.

JEP 356 solves all of this with a unified interface hierarchy and a new family of high-quality algorithms.


The New Interface Hierarchy

RandomGenerator
├── JumpableGenerator        ← can jump ahead by a large amount (2^64 steps)
│   └── LeapableGenerator    ← can leap ahead by an even larger amount (2^128 steps)
│       └── ArbitrarilyJumpableGenerator ← jump by any amount
└── SplittableGenerator      ← can create independent child generators
└── StreamableGenerator      ← can produce streams of generators

All existing PRNG classes implement RandomGenerator:

  • java.util.RandomRandomGenerator
  • java.util.SplittableRandomSplittableGenerator
  • java.util.concurrent.ThreadLocalRandomRandomGenerator (cannot be split)

RandomGenerator: The Common Interface

RandomGenerator defines the full API for consuming random numbers. Write code to this interface and it works with any algorithm:

import java.util.random.RandomGenerator;
import java.util.random.RandomGeneratorFactory;

static void fillWithRandom(int[] array, RandomGenerator rng) {
    for (int i = 0; i < array.length; i++) {
        array[i] = rng.nextInt();
    }
}

// Works with any generator
fillWithRandom(arr, new java.util.Random());
fillWithRandom(arr, RandomGeneratorFactory.of("L64X128MixRandom").create());
fillWithRandom(arr, java.util.concurrent.ThreadLocalRandom.current());

Key Methods on RandomGenerator

RandomGenerator rng = RandomGeneratorFactory.getDefault().create();

rng.nextInt()           // random int (full range)
rng.nextInt(100)        // random int in [0, 100)
rng.nextInt(10, 50)     // random int in [10, 50)
rng.nextLong()          // random long
rng.nextDouble()        // random double in [0.0, 1.0)
rng.nextDouble(1.5)     // random double in [0.0, 1.5)
rng.nextDouble(0.5, 2.0) // random double in [0.5, 2.0)
rng.nextBoolean()       // random boolean
rng.nextFloat()         // random float in [0.0, 1.0)
rng.nextGaussian()      // normally distributed (mean=0, std=1)
rng.nextGaussian(5.0, 2.0) // normally distributed (mean=5, std=2)
rng.nextExponential()   // exponentially distributed (lambda=1)

// Streams
rng.ints(100)           // IntStream of 100 random ints
rng.longs(50)           // LongStream of 50 random longs
rng.doubles(200)        // DoubleStream of 200 random doubles
rng.ints(100, 0, 10)   // 100 random ints in [0, 10)

RandomGeneratorFactory: Algorithm Selection

RandomGeneratorFactory discovers and instantiates generators by algorithm name:

import java.util.random.RandomGeneratorFactory;

// List all available algorithms
RandomGeneratorFactory.all()
    .forEach(f -> System.out.printf("%-30s period=%d stateBits=%d%n",
        f.name(), f.period(), f.stateBits()));

// Instantiate a specific algorithm
RandomGenerator rng = RandomGeneratorFactory.of("L64X128MixRandom").create();

// With a seed (for reproducibility)
RandomGenerator seeded = RandomGeneratorFactory.of("L64X128MixRandom").create(42L);

// Default algorithm
RandomGenerator defaultRng = RandomGeneratorFactory.getDefault().create();

Available Algorithms in Java 17

AlgorithmPeriodState bitsTypeNotes
Random2^4848Legacy; poor quality
SplittableRandom2^6464SplittableGood; no interface before Java 17
ThreadLocalRandom2^6464Thread-local; cannot be passed
L32X64MixRandom2^9696LXM, JumpableSmall state, very fast
L64X128MixRandom2^192192LXM, JumpableRecommended default
L64X128StarStarRandom2^192192LXM, JumpableAlternative to MixRandom
L64X256MixRandom2^320320LXM, JumpableLarge period
L128X128MixRandom2^256256LXM, Jumpable128-bit counter
L128X256MixRandom2^384384LXM, JumpableLarge period + large counter
Xoshiro256PlusPlus2^256256JumpableExcellent equidistribution
Xoroshiro128PlusPlus2^128128JumpableFast, small state

Recommendation: Use L64X128MixRandom as the default for general-purpose work. It has a 2^192 period, excellent statistical properties, and passes all known PRNG tests.


The LXM Algorithm Family

LXM stands for LCG + XBG + Mix function. It combines:

  • LCG (Linear Congruential Generator): A fast counter with good sequential properties
  • XBG (Xor-shift-based generator): High-period generator with excellent equidistribution
  • Mix function: A final mixing step that eliminates any remaining patterns

The combination gives the best of all worlds: the period of the XBG component, the sequential coverage of the LCG component, and the statistical quality of the mix function. All LXM generators pass the BigCrush and PractRand test batteries.


Jumpable Generators

A JumpableGenerator can “jump” ahead by a fixed large amount (typically 2^64 steps) without computing each intermediate value:

import java.util.random.RandomGenerator.JumpableGenerator;

JumpableGenerator rng = (JumpableGenerator) RandomGeneratorFactory
    .of("L64X128MixRandom").create(42L);

// Jump creates a new generator positioned 2^64 steps ahead
// The original rng continues from its current position
JumpableGenerator jumped = rng.jump();

// Each jump produces an independent, non-overlapping sequence
// Use this for parallel workloads: give each thread its own jumped generator

Parallel Processing with Jumps

static int[] parallelFill(int size, long seed) {
    JumpableGenerator master = (JumpableGenerator) RandomGeneratorFactory
        .of("L64X128MixRandom").create(seed);

    int[] result = new int[size];
    int nThreads = Runtime.getRuntime().availableProcessors();
    int chunkSize = size / nThreads;

    // Create independent generators for each thread
    JumpableGenerator[] rngs = new JumpableGenerator[nThreads];
    rngs[0] = master;
    for (int i = 1; i < nThreads; i++) {
        rngs[i] = rngs[i - 1].jump(); // each jumps past the previous
    }

    IntStream.range(0, nThreads).parallel().forEach(t -> {
        int start = t * chunkSize;
        int end = (t == nThreads - 1) ? size : start + chunkSize;
        for (int i = start; i < end; i++) {
            result[i] = rngs[t].nextInt();
        }
    });
    return result;
}

Each thread gets an independent, non-overlapping segment of the generator’s sequence. This is the correct way to parallelize deterministic random computation.


Splittable Generators

A SplittableGenerator creates independent child generators by splitting:

import java.util.random.RandomGenerator.SplittableGenerator;

SplittableGenerator parent = (SplittableGenerator) RandomGeneratorFactory
    .of("L64X128MixRandom").create();

// Split creates a new independent generator
SplittableGenerator child = parent.split();

SplittableRandom (the pre-Java-17 class) now implements this interface:

SplittableGenerator rng = new java.util.SplittableRandom();
SplittableGenerator copy = rng.split();

Splitting is used in recursive parallel algorithms (like ForkJoinPool tasks) where each task splits the generator:

// Recursive parallel computation with independent RNGs
long computeInParallel(SplittableGenerator rng, int depth) {
    if (depth == 0) return rng.nextLong();
    SplittableGenerator left  = rng.split();
    SplittableGenerator right = rng.split();
    return computeInParallel(left, depth - 1) + computeInParallel(right, depth - 1);
}

Streams of Generators

StreamableGenerator produces streams of generators for bulk parallel work:

import java.util.random.RandomGenerator.StreamableGenerator;

StreamableGenerator rng = (StreamableGenerator) RandomGeneratorFactory
    .of("L64X128MixRandom").create();

// Stream of 8 independent generators
rng.rngs(8)
   .parallel()
   .mapToDouble(r -> r.nextDouble())
   .sum();

Reproducibility and Seeds

For testing and debugging, create seeded generators:

RandomGenerator rng1 = RandomGeneratorFactory.of("L64X128MixRandom").create(12345L);
RandomGenerator rng2 = RandomGeneratorFactory.of("L64X128MixRandom").create(12345L);

// Same seed → same sequence
rng1.nextInt() == rng2.nextInt(); // true
rng1.nextInt() == rng2.nextInt(); // true

For production code that requires non-determinism, use the no-argument create() — it seeds from a secure entropy source.


When to Use Each Generator Type

Use caseRecommended generator
General-purpose, single-threadedRandomGeneratorFactory.of("L64X128MixRandom").create()
Single-threaded, small memoryL32X64MixRandom
Multi-threaded, independent streamsJump-based: L64X128MixRandom + jump per thread
Recursive parallel algorithmsSplit-based: SplittableRandom or L64X128MixRandom
Cryptographic usejava.security.SecureRandom (not a RandomGenerator)
Legacy compatibilitynew java.util.Random()
Reproducible simulationAny LXM with explicit seed

Do not use Math.random() in new code — it uses a global Random instance with 48-bit state and no thread-safety guarantees.


Checking Factory Properties Programmatically

RandomGeneratorFactory.all()
    .filter(f -> f.isJumpable())
    .filter(f -> f.stateBits() >= 128)
    .sorted(Comparator.comparingInt(RandomGeneratorFactory::stateBits))
    .forEach(f -> System.out.printf("%-30s period=%s bits=%d%n",
        f.name(), f.period(), f.stateBits()));

Factory methods:

  • f.name() — algorithm name
  • f.period() — period as BigInteger
  • f.stateBits() — number of bits of state
  • f.isJumpable(), f.isSplittable(), f.isStreamable(), f.isHardware()
  • f.equidistribution() — dimension of equidistribution guarantee

Summary

ConceptAPI
Common interfacejava.util.random.RandomGenerator
Algorithm selectionRandomGeneratorFactory.of("L64X128MixRandom")
Create instancefactory.create() (random seed) or factory.create(seed)
DefaultRandomGeneratorFactory.getDefault()
List algorithmsRandomGeneratorFactory.all()
Jump aheadJumpableGenerator.jump()
SplitSplittableGenerator.split()
Stream of generatorsStreamableGenerator.rngs(n)
Recommended algorithmL64X128MixRandom

What’s Next

Article 10: Context-Specific Deserialization Filters (JEP 415) covers how Java 17 extends the JDK’s deserialization security model to support per-context filters — a critical security feature for applications that deserialize untrusted data.