Mastering Random Number Generation: A Complete Guide#
When we work with random number generators, we typically think of a Pseudo Random Number Generator (PRNG) as a mysterious black box:
This black box outputs numbers, usually integers. The beauty lies in how we can transform these results into any data type we need. Want a long? Simply grab two integers - use one for the high 32 bits and another for the low 32 bits. Need boolean, byte, short, or char values? Just take one integer and extract the relevant bits. For floating-point numbers, we can combine multiple random integers according to IEEE standards to construct both the integer and fractional parts.
When we need to constrain the range, the simplest approach is modulo operation plus offset. For example, to get numbers between 1-100, we take the result modulo 99, get the absolute value, then add 1. Since modulo operations are computationally expensive, there’s a neat optimization: check if N & (N-1) equals 0 - if so, N is a power of 2 (powers of 2 in binary look like 100000…, and subtracting 1 gives 011111…, so the AND operation yields 0). Modulo with powers of 2 can be replaced with bitwise AND operations. This is just one simple optimization - real-world implementations are far more sophisticated.
To initialize our black box, we typically use a SEED value. The source of this seed can vary greatly, but let’s set that aside for now and explore the algorithms inside our black box.
Linear Congruential Algorithm#
Let’s start with the most common random number algorithm: Linear Congruential Generator (LCG). It works by multiplying the current seed by coefficient A, adding offset B, then taking modulo C (to keep results within bounds, allowing us to choose appropriate A and B values - we’ll see why this matters later):
X(n+1) = ( A * X(n) + B ) % C
This algorithm shines with its simplicity and decent performance. The values A and B must be carefully chosen to ensure all numbers within range C appear with equal probability. Consider an extreme example: A = 2, B = 2, C = 10. Here, odd numbers like 1, 3, 5, 7, 9 would never appear in subsequent iterations. To calculate suitable A and B values, we need to constrain C within manageable bounds. For computational efficiency, C is typically set to powers of 2, allowing modulo operations to be optimized into bitwise AND operations. Fortunately, mathematical wizards have already discovered these magic numbers for us - we can simply use their findings!
This algorithm generates deterministic random sequences - if X leads to Y, then Y always leads to Z. Think of it as a deterministic loop:
The size of this loop is called the Period. With sufficiently large periods and varying initial seeds, we achieve pseudo-randomness. However, when we need multiple random number generators, things get tricky. While we can ensure different initial seeds for each generator, we can’t guarantee that one generator’s seed isn’t just a few steps away from another’s in the sequence. For instance, if one generator starts with seed X and another with Z, even though X and Z might seem vastly different, they could be separated by just one step Y in the algorithm’s sequence. Such generators would perform poorly.
How can we ensure significant separation between different random number generators? We need simple calculations (not computing millions of iterations) to directly create seeds with large gaps. This property is called jumpability. The Xoshiro algorithm, based on Linear Feedback Shift Register algorithms, provides us with jumpable random number generation.
Linear Feedback Shift Register Algorithm#
A Linear Feedback Shift Register (LFSR) uses the linear function of its previous state’s output as the next input. XOR operations are the most common single-bit linear functions: certain register bits are XORed together as input, then all bits in the register are shifted.
Choosing which bits to use is an art form. Current popular implementations include the XorShift algorithm and its further optimized successor, the Xoshiro family. Xoshiro algorithms are relatively new, optimized random number algorithms with simple calculations, excellent performance, and jumpability.
These algorithms are jumpable. When creating two random number generators with significant separation, we can use a random initial seed for one generator, then use the algorithm’s jump operation to directly generate a well-separated seed for another generator.
Another interesting feature: Linear Congruential algorithms aren’t reversible - we can only derive X(n+1) from X(n), not the reverse. This works well for applications like random playlist shuffling, where we only need the current random number to determine the next song. Linear Feedback Shift Register algorithms can be made reversible.
However, LFSR algorithms have limitations when generating different random sequence generators. They still come from the same loop - even with jump operations creating separation, uneven pressure over time might cause different generators to converge to the same seed again. Are there algorithms that can generate completely different random sequence loops?
DotMix Algorithm#
The DotMix algorithm offers a different approach: given an initial seed, set a fixed step M, and for each random generation, add step M to the seed, then pass it through a hash function to map the value to a hash result:
X(n+1) = HASH(X(n) + M)
This algorithm demands high-quality hash functions that produce dramatically different outputs from slight input changes. The SplitMix algorithm, based on DotMix, uses the MurMurHash3 algorithm - this is the underlying principle of Java 8’s SplittableRandom
.
The beauty of this algorithm lies in how we can easily ensure different parameter generators produce different sequences. For example, one might generate 1, 4, 3, 7… while another produces 1, 5, 3, 2. This is something Linear Congruential algorithms can’t achieve - their sequences are deterministic regardless of seed changes, and we can’t freely modify A, B, C values without risking incomplete number coverage. The same applies to Xoshiro. SplitMix algorithms don’t have this concern - different seeds and step sizes M guarantee different sequences. This ability to generate different sequences is called splittability.
This explains why SplittableRandom
is better suited for multithreading than Random
(which uses Linear Congruential):
- Multiple threads sharing one
Random
: ensures sequence randomness but suffers CompareAndSet performance costs for seed updates - Each thread using
Random
with identical seeds: produces identical random sequences per thread - Each thread using
Random
with different seeds: we can’t guarantee one thread’s seed isn’t just the next result (or within a few steps) of another thread’s seed. With uneven thread pressure (thread pools often have only some threads working during quiet periods), certain threads might end up with identical random sequences
Using SplittableRandom
, simply call the split
interface to assign different parameters to different threads, and different parameters virtually guarantee different sequences.
Thought Exercise: How Do We Generate Sequences with Periods Larger Than Number Capacity?#
The simplest approach: merge two sequences with periods equal to capacity through round-robin, creating a sequence with Period = capacity + capacity
:
We can also record results from two sequences and combine them using operations like XOR or hashing. This gives us Period = capacity * capacity
.
For further expansion, we can use these methods to combine different algorithmic sequences, gaining the random advantages of each algorithm. Java 17’s LXM algorithm exemplifies this approach.
LXM Algorithm#
Introduced in Java 17, the LXM algorithm (L for Linear Congruential, X for Xoshiro, M for MurMurHash) is relatively simple, combining Linear Congruential and Xoshiro algorithms, then hashing with MurMurHash:
- L34X64M: uses one 32-bit number for Linear Congruential results, two 32-bit numbers for Xoshiro results, then MurMurHash combines these into one 64-bit number
- L128X256M: uses two 64-bit numbers for Linear Congruential results, four 64-bit numbers for Xoshiro results, then MurMurHash combines these into one 64-bit number
LXM algorithms achieve splittability through MurMurHash but don’t retain Xoshiro’s jumpability.
Seed Sources#
Since all JDK random algorithms are based on previous inputs, using fixed seeds always produces identical random sequences. This isn’t suitable for security-sensitive scenarios. The official definition of cryptographically secure
requires unpredictable seeds producing non-deterministic outputs.
Linux collects user input, system interrupts, and other runtime data to generate random seeds stored in pools that programs can access. However, these pools only generate after collecting sufficient data, have limited size, and don’t guarantee good random distribution. We can’t use them directly for random numbers but as seeds for our random number generators. Linux abstracts these pools as two files: /dev/random
and /dev/urandom
. One blocks until sufficient entropy is collected, the other returns whatever’s available.
Before Linux 4.8:
After Linux 4.8:
When entropy pools are insufficient, /dev/random
blocks while /dev/urandom
doesn’t. For most purposes, /dev/urandom
suffices, so we typically use the JVM parameter -Djava.security.egd=file:/dev/./urandom
to reduce blocking.
We can also use business characteristics to periodically reset all Random seeds, further increasing crack resistance. For example, use the past hour’s active users × order count as the new seed every hour.
Testing Random Algorithm Randomness#
All these algorithms implement pseudo-randomness - current random results strongly correlate with previous ones. Virtually all fast random algorithms work this way.
Even with sufficiently secret seeds, knowing the algorithm still allows predicting next outputs from current ones, or reverse-engineering algorithms from multiple results to predict future outputs.
For pseudo-random algorithms, we need to verify generated random numbers satisfy characteristics like:
- Maximum possible period: the full cycle or period refers to the number of results needed for the random sequence to traverse all possible outcomes and return to the initial seed. This period should be as long as possible
- Equidistribution: each possible result should appear the same number of times within one period, ensuring fairness in applications like lotteries
- Complexity testing: generated sequences should be sufficiently complex, avoiding regular patterns like arithmetic or geometric progressions
- Security testing: difficulty in reverse-engineering the algorithm from relatively few results
Many frameworks now test and evaluate random sequences from specific algorithms:
- testU01 randomness testing: https://github.com/umontreal-simul/TestU01-2009/
- NIST randomness testing: https://nvlpubs.nist.gov/nistpubs/legacy/sp/nistspecialpublication800-22r1a.pdf
- DieHarder Suite randomness testing
Java’s built-in random algorithms basically pass most testU01 tests. Currently, the optimization algorithms mentioned above all expose some randomness issues to varying degrees. Java 17’s LXM algorithm performs best in randomness testing - note this refers to randomness performance, not computational performance.
All Random Algorithms in Java (Excluding SecureRandom)#
- Linear Congruential generator: https://doi.org/10.1093%2Fcomjnl%2F1.2.83
- Linear-feedback shift register: https://www.ams.org/journals/mcom/1965-19-090/S0025-5718-1965-0184406-1/S0025-5718-1965-0184406-1.pdf
- XORShift: https://doi.org/10.18637%2Fjss.v008.i14
- Xoroshiro128+: https://arxiv.org/abs/1805.01407
- LXM: https://dl.packetstormsecurity.net/papers/general/Google_Chrome_3.0_Beta_Math.random_vulnerability.pdf
- SplitMix: http://gee.cs.oswego.edu/dl/papers/oopsla14.pdf
Why We Rarely Consider Random Security in Real Business Applications#
Mainly because we typically deploy multiple instances with load balancing and multithreading. Generally, each thread uses Random instances with different initial seeds (like ThreadLocalRandom). For random-sensitive businesses like lotteries, individual users usually have attempt limits, making it difficult to collect enough results to reverse-engineer algorithms and predict next results - plus you’d be competing with other users. We typically limit random number ranges rather than using raw random numbers, greatly increasing reverse-engineering difficulty. Finally, we can periodically use real-time business metrics to reset seeds, like using (active users × orders) from the past hour as the new seed every hour.
Therefore, in real business scenarios, we rarely use SecureRandom. If we want initial seeds that even programmers can’t guess (timestamps are predictable), we can specify random class initial seed sources through the JVM parameter -Djava.util.secureRandomSeed=true
. This affects all Java random number generators (Random, SplittableRandom, ThreadLocalRandom, etc.).
Corresponding source code:
static {
String sec = VM.getSavedProperty("java.util.secureRandomSeed");
if (Boolean.parseBoolean(sec)) {
//Get initial SEED from SecureRandom
// SecureRandom's SEED source in Linux is /dev/random or /dev/urandom specified by java.security.egd
byte[] seedBytes = java.security.SecureRandom.getSeed(8);
long s = (long)seedBytes[0] & 0xffL;
for (int i = 1; i < 8; ++i)
s = (s << 8) | ((long)seedBytes[i] & 0xffL);
seeder.set(s);
}
}
So for our business needs, we mainly care about algorithm performance and randomness equidistribution, and since tested algorithms generally have good randomness, we primarily focus on performance.
For security-sensitive businesses like SSL encryption and cryptographic random hash generation, higher security randomness is needed. That’s when we consider SecureRandom, which uses more complex algorithms involving cryptographic concepts - we won’t delve into these secure Random algorithms here.
Random Number Generation Before Java 17#
First, here’s the correspondence between algorithms and implementation classes:
Using JDK APIs#
1. Using java.util.Random
and related APIs:
Random random = new Random();
random.nextInt();
Math.random()
is also based on Random
java.lang.Math
:
public static double random() {
return RandomNumberGeneratorHolder.randomNumberGenerator.nextDouble();
}
private static final class RandomNumberGeneratorHolder {
static final Random randomNumberGenerator = new Random();
}
Random
is designed to be thread-safe because the seed is atomic and randomization just CAS-updates this seed:
java.util.Random
:
protected int next(int bits) {
long oldseed, nextseed;
AtomicLong seed = this.seed;
do {
oldseed = seed.get();
nextseed = (oldseed * multiplier + addend) & mask;
} while (!seed.compareAndSet(oldseed, nextseed));
return (int)(nextseed >>> (48 - bits));
}
This also shows Random is based on Linear Congruential algorithms
2. Using java.util.SplittableRandom
and related APIs
SplittableRandom splittableRandom = new SplittableRandom();
splittableRandom.nextInt();
As analyzed earlier, SplittableRandom implements the SplitMix algorithm - given an initial seed, set a fixed step M, add step M to the seed each time, then hash the value (using MurMurHash3) to map it to a hash result.
SplittableRandom
is not thread-safe:
java.util.SplittableRandom
:
public int nextInt() {
return mix32(nextSeed());
}
private long nextSeed() {
//Not thread-safe here
return seed += gamma;
}
ThreadLocalRandom
is based on SplittableRandom
, and we use ThreadLocalRandom
in multithreaded environments:
ThreadLocalRandom.current().nextInt();
SplittableRandom
can return a completely new SplittableRandom
with different parameters and vastly different random sequence characteristics through the split method. We can use these for different threads, which is very common in parallel streams:
IntStream.range(0, 1000)
.parallel()
.map(index -> usersService.getUsersByGood(index))
.map(users -> users.get(splittableRandom.split().nextInt(users.size())))
.collect(Collectors.toList());
However, due to lack of cache line padding and other multithreading performance optimizations, its multithreaded performance is still worse than ThreadLocalRandom
, which is based on SplittableRandom
.
3. Using java.security.SecureRandom
for higher security randomness
SecureRandom drbg = SecureRandom.getInstance("DRBG");
drbg.nextInt();
These algorithms are typically based on cryptographic algorithms with more complex calculations and poorer performance. They’re only used for highly security-sensitive businesses, not general applications like lotteries.
Performance Testing#
Single-threaded testing:
Benchmark Mode Cnt Score Error Units
TestRandom.testDRBGSecureRandomInt thrpt 50 940907.223 ± 11505.342 ops/s
TestRandom.testDRBGSecureRandomIntWithBound thrpt 50 992789.814 ± 71312.127 ops/s
TestRandom.testRandomInt thrpt 50 106491372.544 ± 8881505.674 ops/s
TestRandom.testRandomIntWithBound thrpt 50 99009878.690 ± 9411874.862 ops/s
TestRandom.testSplittableRandomInt thrpt 50 295631145.320 ± 82211818.950 ops/s
TestRandom.testSplittableRandomIntWithBound thrpt 50 190550282.857 ± 17108994.427 ops/s
TestRandom.testThreadLocalRandomInt thrpt 50 264264886.637 ± 67311258.237 ops/s
TestRandom.testThreadLocalRandomIntWithBound thrpt 50 162884175.411 ± 12127863.560 ops/s
Multithreaded testing:
Benchmark Mode Cnt Score Error Units
TestRandom.testDRBGSecureRandomInt thrpt 50 2492896.096 ± 19410.632 ops/s
TestRandom.testDRBGSecureRandomIntWithBound thrpt 50 2478206.361 ± 111106.563 ops/s
TestRandom.testRandomInt thrpt 50 345345082.968 ± 21717020.450 ops/s
TestRandom.testRandomIntWithBound thrpt 50 300777199.608 ± 17577234.117 ops/s
TestRandom.testSplittableRandomInt thrpt 50 465579146.155 ± 25901118.711 ops/s
TestRandom.testSplittableRandomIntWithBound thrpt 50 344833166.641 ± 30676425.124 ops/s
TestRandom.testThreadLocalRandomInt thrpt 50 647483039.493 ± 120906932.951 ops/s
TestRandom.testThreadLocalRandomIntWithBound thrpt 50 467680021.387 ± 82625535.510 ops/s
Results align with our expectations: ThreadLocalRandom
performs best in multithreaded environments. In single-threaded environments, SplittableRandom
and ThreadLocalRandom
are comparable, both outperforming others. SecureRandom
performs hundreds of times worse than others.
Test code follows (Note: although Random and SecureRandom are thread-safe, ThreadLocal is still used to avoid excessive performance degradation from compareAndSet):
package prng;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.util.Random;
import java.util.SplittableRandom;
import java.util.concurrent.ThreadLocalRandom;
import org.openjdk.jmh.annotations.Benchmark;
import org.openjdk.jmh.annotations.BenchmarkMode;
import org.openjdk.jmh.annotations.Fork;
import org.openjdk.jmh.annotations.Measurement;
import org.openjdk.jmh.annotations.Mode;
import org.openjdk.jmh.annotations.Scope;
import org.openjdk.jmh.annotations.State;
import org.openjdk.jmh.annotations.Threads;
import org.openjdk.jmh.annotations.Warmup;
import org.openjdk.jmh.infra.Blackhole;
import org.openjdk.jmh.runner.Runner;
import org.openjdk.jmh.runner.RunnerException;
import org.openjdk.jmh.runner.options.Options;
import org.openjdk.jmh.runner.options.OptionsBuilder;
//Test metric is throughput
@BenchmarkMode(Mode.Throughput)
//Warmup needed to exclude JIT compilation and JVM metric collection effects
@Warmup(iterations = 1)
//Thread count
@Threads(10)
@Fork(1)
//Test iterations
@Measurement(iterations = 50)
//Class instance lifecycle shared across all test threads
@State(value = Scope.Benchmark)
public class TestRandom {
ThreadLocal<Random> random = ThreadLocal.withInitial(Random::new);
ThreadLocal<SplittableRandom> splittableRandom = ThreadLocal.withInitial(SplittableRandom::new);
ThreadLocal<SecureRandom> drbg = ThreadLocal.withInitial(() -> {
try {
return SecureRandom.getInstance("DRBG");
}
catch (NoSuchAlgorithmException e) {
throw new IllegalArgumentException(e);
}
});
@Benchmark
public void testRandomInt(Blackhole blackhole) throws Exception {
blackhole.consume(random.get().nextInt());
}
@Benchmark
public void testRandomIntWithBound(Blackhole blackhole) throws Exception {
//Avoid 2^n numbers as they're optimized but not typical application ranges
blackhole.consume(random.get().nextInt(1, 100));
}
@Benchmark
public void testSplittableRandomInt(Blackhole blackhole) throws Exception {
blackhole.consume(splittableRandom.get().nextInt());
}
@Benchmark
public void testSplittableRandomIntWithBound(Blackhole blackhole) throws Exception {
//Avoid 2^n numbers as they're optimized but not typical application ranges
blackhole.consume(splittableRandom.get().nextInt(1, 100));
}
@Benchmark
public void testThreadLocalRandomInt(Blackhole blackhole) throws Exception {
blackhole.consume(ThreadLocalRandom.current().nextInt());
}
@Benchmark
public void testThreadLocalRandomIntWithBound(Blackhole blackhole) throws Exception {
//Avoid 2^n numbers as they're optimized but not typical application ranges
blackhole.consume(ThreadLocalRandom.current().nextInt(1, 100));
}
@Benchmark
public void testDRBGSecureRandomInt(Blackhole blackhole) {
blackhole.consume(drbg.get().nextInt());
}
@Benchmark
public void testDRBGSecureRandomIntWithBound(Blackhole blackhole) {
//Avoid 2^n numbers as they're optimized but not typical application ranges
blackhole.consume(drbg.get().nextInt(1, 100));
}
public static void main(String[] args) throws RunnerException {
Options opt = new OptionsBuilder().include(TestRandom.class.getSimpleName()).build();
new Runner(opt).run();
}
}
Changes in Java 17#
Limitations of Previous APIs#
- No unified interface: Previous
Random
andSplittableRandom
lacked common inheritance or abstract interfaces. While their methods are largely identical and replacement isn’t too troublesome, this makes implementing custom random algorithms difficult due to the lack of unified interfaces. - ThreadLocalRandom doesn’t play well with future Project Loom virtual threads. Virtual threads are continuously creatable resources, and using ThreadLocalRandom in a one-to-one mapping with numerous virtual threads would weaken randomness. We need new implementation methods and should start paving the way for Project Loom.
New API Definitions#
Java 17’s JEP 356: Enhanced Pseudo-Random Number Generators unified random number generator interfaces under RandomGenerator
:
For jumpability (simple calculations to skip many elements in sequence loops), there’s the JumpableGenerator
interface, and for larger jump steps, LeapableGenerator
.
For splittability (simple calculations to split generators producing completely different sequences), there’s the SplitableGenerator
interface.
The correspondence between algorithms and implementation classes:
After unification, we can create different random number generators like this:
RandomGenerator random = RandomGeneratorFactory.of("Random").create();
RandomGenerator secureRandom = RandomGeneratorFactory.of("SecureRandom").create();
RandomGenerator splittableRandom = RandomGeneratorFactory.of("SplittableRandom").create();
RandomGenerator xoroshiro128PlusPlus = RandomGeneratorFactory.of("Xoroshiro128PlusPlus").create();
RandomGenerator xoshiro256PlusPlus = RandomGeneratorFactory.of("Xoshiro256PlusPlus").create();
RandomGenerator l64X256MixRandom = RandomGeneratorFactory.of("L64X256MixRandom").create();
RandomGenerator l64X128StarStarRandom = RandomGeneratorFactory.of("L64X128StarStarRandom").create();
RandomGenerator l64X128MixRandom = RandomGeneratorFactory.of("L64X128MixRandom").create();
RandomGenerator l64X1024MixRandom = RandomGeneratorFactory.of("L64X1024MixRandom").create();
RandomGenerator l32X64MixRandom = RandomGeneratorFactory.of("L32X64MixRandom").create();
RandomGenerator l128X256MixRandom = RandomGeneratorFactory.of("L128X256MixRandom").create();
RandomGenerator l128X128MixRandom = RandomGeneratorFactory.of("L128X128MixRandom").create();
RandomGenerator l128X1024MixRandom = RandomGeneratorFactory.of("L128X1024MixRandom").create();
Properties of Each Algorithm’s Random Number Generator Classes#
1.Random
: Based on Linear Congruential algorithm generating 48-bit numbers, with parameters ensuring every number can be generated, so Period = 2^48
. nextInt and nextLong cannot achieve perfectly uniform random distribution because generated numbers are 48-bit, nextInt takes 32 bits, and nextLong combines two calls. As analyzed earlier, this algorithm cannot jump or split.
2.SplittableRandom
: Based on SplitMix algorithm generating 64-bit numbers, MurMurHash ensures every number in range appears (Period = 2^64
) with perfect uniform distribution. For nextInt, every result appears twice per period; for nextLong, every result appears once per period. As analyzed, this algorithm cannot jump but can split.
3.Xoroshiro128PlusPlus
: Based on Xoroshiro128++
algorithm using two 64-bit numbers for state, these two numbers are never simultaneously zero, combining into one 64-bit random number. With two 64-bit numbers, there are 2^64 * 2^64 = 2^128
combinations, minus one (both zero), so Period = 2^128 - 1
. This algorithm can jump but cannot split.
4.Xoshiro256PlusPlus
: Based on Xoshiro256++
algorithm using four 64-bit numbers for state, these four numbers are never simultaneously zero, combining into one 64-bit random number. With four 64-bit numbers, there are 2^256
combinations, minus one, so Period = 2^256 - 1
. This algorithm can jump but cannot split.
5. L64X256MixRandom
: Based on LXM algorithm using one 64-bit number for Linear Congruential results and four 64-bit numbers for Xoshiro
combination. Linear Congruential has 2^64
combinations, Xoshiro
has 2^256 - 1
combinations, totaling 2^64(2^256 - 1)
combinations, so Period = 2^64(2^256 - 1)
. This algorithm can split but cannot jump.
Other LXM implementations are similar.
You can see these properties in each algorithm implementation class’s annotations:
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface RandomGeneratorProperties {
/**
* Algorithm name
*/
String name();
/**
* Algorithm category
*/
String group() default "Legacy";
/**
* Period size described by i, j, k:
* period = (2^i - j) * 2^k
*/
int i() default 0;
int j() default 0;
int k() default 0;
/**
* Equidistribution, 0 or max means non-uniform distribution
* This value describes how many times each number appears in one period
* Not perfectly precise, ignores small deviations
* e.g., Xoroshiro128++ considers each number appears 2^64 times, not 2^64 - 1
*/
int equidistribution() default Integer.MAX_VALUE;
/**
* Whether based on system Entropy (see SEED sources section)
*/
boolean isStochastic() default false;
/**
* Whether hardware-assisted algorithm
*/
boolean isHardware() default false;
}
We can also view each implementation’s properties and filter algorithms for our business needs through these APIs:
RandomGeneratorFactory.all()
.map(fac -> fac.group()+":"+fac.name()
+ " {"
+ (fac.isSplittable()?" splitable":"")
+ (fac.isStreamable()?" streamable":"")
+ (fac.isJumpable()?" jumpable":"")
+ (fac.isLeapable()?" leapable":"")
+ (fac.isHardware()?" hardware":"")
+ (fac.isStatistical()?" statistical":"")
+ (fac.isStochastic()?" stochastic":"")
+ " stateBits: "+fac.stateBits()
+ " }"
)
.sorted().forEach(System.out::println);
Output:
LXM:L128X1024MixRandom { splitable streamable statistical stateBits: 1152 }
LXM:L128X128MixRandom { splitable streamable statistical stateBits: 256 }
LXM:L128X256MixRandom { splitable streamable statistical stateBits:384 }
LXM:L32X64MixRandom { splitable streamable statistical stateBits: 96 }
LXM:L64X1024MixRandom { splitable streamable statistical stateBits: 1088 }
LXM:L64X128MixRandom { splitable streamable statistical stateBits: 192 }
LXM:L64X128StarStarRandom { splitable streamable statistical stateBits: 192 }
LXM:L64X256MixRandom { splitable streamable statistical stateBits: 320 }
Legacy:Random { statistical stateBits: 48 }
Legacy:SecureRandom { stochastic stateBits: 2147483647 }
Legacy:SplittableRandom { splitable streamable statistical stateBits: 64 }
Xoroshiro:Xoroshiro128PlusPlus { streamable jumpable leapable statistical stateBits: 128 }
Xoshiro:Xoshiro256PlusPlus { streamable jumpable leapable statistical stateBits: 256 }
Which Algorithm is Fastest (Pretty Obvious Without Testing)#
Based on our previous analysis, SplittableRandom should still be fastest in single-threaded environments, with ThreadLocalRandom fastest in multithreaded scenarios. The new random algorithm implementations require more computation as periods increase, and LXM implementations need additional calculations. These algorithms were added to accommodate more random applications, not for speed. However, to satisfy curiosity, here’s test code demonstrating the new RandomGenerator API’s convenience:
package prng;
import java.util.random.RandomGenerator;
import java.util.random.RandomGeneratorFactory;
import org.openjdk.jmh.annotations.Benchmark;
import org.openjdk.jmh.annotations.BenchmarkMode;
import org.openjdk.jmh.annotations.Fork;
import org.openjdk.jmh.annotations.Measurement;
import org.openjdk.jmh.annotations.Mode;
import org.openjdk.jmh.annotations.Param;
import org.openjdk.jmh.annotations.Scope;
import org.openjdk.jmh.annotations.Setup;
import org.openjdk.jmh.annotations.State;
import org.openjdk.jmh.annotations.Threads;
import org.openjdk.jmh.annotations.Warmup;
import org.openjdk.jmh.infra.Blackhole;
import org.openjdk.jmh.runner.Runner;
import org.openjdk.jmh.runner.RunnerException;
import org.openjdk.jmh.runner.options.Options;
import org.openjdk.jmh.runner.options.OptionsBuilder;
//Test metric is throughput
@BenchmarkMode(Mode.Throughput)
//Warmup needed to exclude JIT compilation and JVM metric collection effects
@Warmup(iterations = 1)
//Thread count
@Threads(10)
@Fork(1)
//Test iterations
@Measurement(iterations = 50)
//Class instance lifecycle shared across all test threads
@State(value = Scope.Benchmark)
public class TestRandomGenerator {
@Param({
"Random", "SecureRandom", "SplittableRandom", "Xoroshiro128PlusPlus", "Xoshiro256PlusPlus", "L64X256MixRandom",
"L64X128StarStarRandom", "L64X128MixRandom", "L64X1024MixRandom", "L32X64MixRandom", "L128X256MixRandom",
"L128X128MixRandom", "L128X1024MixRandom"
})
private String name;
ThreadLocal<RandomGenerator> randomGenerator;
@Setup
public void setup() {
final String finalName = this.name;
randomGenerator = ThreadLocal.withInitial(() -> RandomGeneratorFactory.of(finalName).create());
}
@Benchmark
public void testRandomInt(Blackhole blackhole) throws Exception {
blackhole.consume(randomGenerator.get().nextInt());
}
@Benchmark
public void testRandomIntWithBound(Blackhole blackhole) throws Exception {
//Avoid 2^n numbers as they're optimized but not typical application ranges
blackhole.consume(randomGenerator.get().nextInt(1, 100));
}
public static void main(String[] args) throws RunnerException {
Options opt = new OptionsBuilder().include(TestRandomGenerator.class.getSimpleName()).build();
new Runner(opt).run();
}
}
Test results:
Benchmark (name) Mode Cnt Score Error Units
TestRandomGenerator.testRandomInt Random thrpt 50 276250026.985 ± 240164319.588 ops/s
TestRandomGenerator.testRandomInt SecureRandom thrpt 50 2362066.269 ± 1277699.965 ops/s
TestRandomGenerator.testRandomInt SplittableRandom thrpt 50 365417656.247 ± 377568150.497 ops/s
TestRandomGenerator.testRandomInt Xoroshiro128PlusPlus thrpt 50 341640250.941 ± 287261684.079 ops/s
TestRandomGenerator.testRandomInt Xoshiro256PlusPlus thrpt 50 343279172.542 ± 247888916.092 ops/s
TestRandomGenerator.testRandomInt L64X256MixRandom thrpt 50 317749688.838 ± 245196331.079 ops/s
TestRandomGenerator.testRandomInt L64X128StarStarRandom thrpt 50 294727346.284 ± 283056025.396 ops/s
TestRandomGenerator.testRandomInt L64X128MixRandom thrpt 50 314790625.909 ± 257860657.824 ops/s
TestRandomGenerator.testRandomInt L64X1024MixRandom thrpt 50 315040504.948 ± 101354716.147 ops/s
TestRandomGenerator.testRandomInt L32X64MixRandom thrpt 50 311507435.009 ± 315893651.601 ops/s
TestRandomGenerator.testRandomInt L128X256MixRandom thrpt 50 187922591.311 ± 137220695.866 ops/s
TestRandomGenerator.testRandomInt L128X128MixRandom thrpt 50 218433110.870 ± 164229361.010 ops/s
TestRandomGenerator.testRandomInt L128X1024MixRandom thrpt 50 220855813.894 ± 47531327.692 ops/s
TestRandomGenerator.testRandomIntWithBound Random thrpt 50 248088572.243 ± 206899706.862 ops/s
TestRandomGenerator.testRandomIntWithBound SecureRandom thrpt 50 1926592.946 ± 2060477.065 ops/s
TestRandomGenerator.testRandomIntWithBound SplittableRandom thrpt 50 334863388.450 ± 92778213.010 ops/s
TestRandomGenerator.testRandomIntWithBound Xoroshiro128PlusPlus thrpt 50 252787781.866 ± 200544008.824 ops/s
TestRandomGenerator.testRandomIntWithBound Xoshiro256PlusPlus thrpt 50 247673155.126 ± 164068511.968 ops/s
TestRandomGenerator.testRandomIntWithBound L64X256MixRandom thrpt 50 273735605.410 ± 87195037.181 ops/s
TestRandomGenerator.testRandomIntWithBound L64X128StarStarRandom thrpt 50 291151383.164 ± 192343348.429 ops/s
TestRandomGenerator.testRandomIntWithBound L64X128MixRandom thrpt 50 217051928.549 ± 177462405.951 ops/s
TestRandomGenerator.testRandomIntWithBound L64X1024MixRandom thrpt 50 222495366.798 ± 180718625.063 ops/s
TestRandomGenerator.testRandomIntWithBound L32X64MixRandom thrpt 50 305716905.710 ± 51030948.739 ops/s
TestRandomGenerator.testRandomIntWithBound L128X256MixRandom thrpt 50 174719656.589 ± 148285151.049 ops/s
TestRandomGenerator.testRandomIntWithBound L128X128MixRandom thrpt 50 176431895.622 ± 143002504.266 ops/s
TestRandomGenerator.testRandomIntWithBound L128X1024MixRandom thrpt 50 198282642.786 ± 24204852.619 ops/s
From previous results, we already knew SplittableRandom performs best in single-threaded scenarios, while ThreadLocalRandom (similar algorithm but with multithreading optimizations) performs best in multithreaded environments.
How to Choose Random Algorithms#
The principle: examine your business scenario to determine total possible random combinations and their range. Then find the algorithm with the best performance among those with periods larger than this range. For example, if your business scenario involves a deck of 52 cards (excluding jokers) with random dealing order:
- First card:
randomGenerator.nextInt(0, 52)
, choose from remaining 52 cards - Second card:
randomGenerator.nextInt(0, 51)
, choose from remaining 51 cards - And so on
This gives us 52! total results, ranging between 2^225 ~ 2^226. If our random number generator’s period is smaller than this result set, certain card orders might never be generated. Therefore, we need a random number generator with Period > 52!.
Future Outlook#
Random Number Generators in Project Loom#
If Project Loom doesn’t optimize ThreadLocal, ThreadLocalRandom’s randomness performance will also degrade because virtual threads are object-like entities that can be continuously created and recycled. ThreadLocalRandom can’t enumerate infinitely. We might need to use fixed multiple random number generators for all virtual threads instead of ThreadLocalRandom:
ExecutorService vte = Executors.newVirtualThreadExecutor();
SplittableGenerator source = RandomGeneratorFactory.<SplittableGenerator>of("L128X1024MixRandom").create();
//Split into 128 different random number generators
List<RandomGenerator> rngs = source.splits(128);
AtomicInteger i = new AtomicInteger(0);
vte.submit(() -> {
long random = rngs.get(Math.abs(i.incrementAndGet() & 127)).nextLong();
...
});
Random Number Generators with Scoped Local Features#
Scoped Local is a more general domain variable (like ThreadLocal for current thread domain, Scoped Local can support different domains including virtual threads, threads, method domains, package domains, etc.). No version has been announced yet, but based on current leaks, we could use this approach to better assign random number generators to each thread:
private final static ScopeLocal<SplittableGenerator> rng_scope =
ScopeLocal.inheritableForType(SplittableGenerator.class);
public static void main(String[] args) throws InterruptedException {
SplittableGenerator rng1 =
RandomGeneratorFactory.<SplittableGenerator>of("L128X1024MixRandom").create();
SplittableGenerator rng2 =
RandomGeneratorFactory.<SplittableGenerator>of("L32X64MixRandom").create();
try (ExecutorService vte = Executors.newVirtualThreadExecutor()) {
for (int i = 0; i < 5; i++) {
ScopeLocal.where(rng_scope, rng1.split(), () -> { vte.submit(new Task()); });
}
for (int i = 0; i < 5; i++) {
ScopeLocal.where(rng_scope, rng2.split(), () -> { vte.submit(new Task()); });
}
}
}
private static class Task implements Runnable {
@Override public void run() {
SplittableGenerator rng = rng_scope.get();
System.out.println(rng);
}
}
This comprehensive guide covers the evolution of random number generation in Java, from basic concepts to advanced implementations, providing practical insights for choosing the right algorithm for your specific use case. Whether you’re building simple applications or complex systems requiring high-quality randomness, understanding these fundamentals will help you make informed decisions about random number generation in your projects.