
Generating random numbers (or strings or other random objects) is one of the most common programming tasks.
This article describes:
- which possibilities there are in Java to generate random numbers,
- why these are so-called “pseudorandom numbers”,
- what technology is behind their generation,
- how to predict random numbers in Java,
- how the implementation of the various methods has changed throughout Java versions,
- and what the
RandomGeneratorinterface introduced in Java 17 changes.
Experienced Java developers familiar with the various ways to create random values can skip directly to the “Pseudorandom Number Generation” or “Changes in Implementations Over Time” section.
You can find the code samples for this article in this GitHub repository.
How to Generate Random Numbers
This chapter shows the fundamental classes and methods for generating a random number in Java and what to consider when using them in terms of thread safety.
Java Math.random() Method
One of the oldest methods (it has existed since Java 1.0) to generate a random double number is to call Math.random():
double d = Math.random();
The call returns a random number between 0 and 1. More precisely: a double floating-point number greater than or equal to 0.0 and less than 1.0.
Math.random() is thread-safe according to the documentation. However, synchronization is broken from Java 1.3 up to and including Java 7. If you work with one of these versions, you must not call Math.random() from different threads.
Internally, Math.random() calls the nextDouble() method of a static instance of the Random class held in the Math class, which is discussed in the next section.
For more details on Math.random()’s internal functionality and thread safety, see the chapter on implementing this method.
Java Random Class
Also present since Java 1.0 is the java.util.Random class. With it, you can generate a random int number as follows:
Random random = new Random();
int i = random.nextInt();
The call returns a random integer number in the range of Integer.MIN_VALUE (= -231 = -2,147,483,648) to Integer.MAX_VALUE (= 231-1 = 2,147,483,647).
Other methods for generating random values are:
nextInt(int bound)– generates a random number greater than or equal to 0 and less than the specified upperbound.nextInt(int origin, int bound)– generates a random number greater than or equal tooriginand less thanbound(since Java 17, see the section “Generate a Random Number in a Range”).nextLong()– generates a randomlongvalue.nextBoolean()– returns a randombooleanvalue, i.e.,trueorfalse.nextFloat()– returns a randomfloatnumber greater than or equal to 0.0 and less than 1.0.nextDouble()– returns a randomdoublenumber greater than or equal to 0.0 and less than 1.0 (this method is called fromMath.random(), as described in the previous section).nextBytes(byte[] bytes)– fills the specifiedbytearray with random bytes.nextGaussian()– returns a Gaussian-distributed random number with a standard deviation of 1.0, i.e., according to the normal distribution, the probability of a number close to 0 is higher than the probability of a number far from 0.
You can use the setSeed(long seed) method or the second constructor Random(long seed) to set the so-called “seed” value of the random number generator. This is only necessary for special requirements. You can read more about this in the chapter on Pseudorandom Numbers.
Extensions to java.util.Random in Java 8
With the introduction of streams in Java 8, java.util.Random has been extended to include methods for generating random number streams. The Random.ints() method generates an IntStream: a stream of random int values.
The following example prints seven random numbers to the console:
Random random = new Random();
random.ints().limit(7).forEach(System.out::println);
The limitation to seven elements set by limit(7) can also be requested in an overloaded variant of the ints() method:
Random random = new Random();
random.ints(7).forEach(System.out::println);
Two further variants allow the specification of lower and upper bounds of the generated values. The following example generates seven random numbers greater than or equal to 0 and less than 1,000 – once limited by limit() and once by the first parameter of the ints() method.
Random random = new Random();
random.ints(0, 1000).limit(7).forEach(System.out::println);
random.ints(7, 0, 1000).forEach(System.out::println);
Corresponding four variants exist for:
longs()– generates aLongStreamof random numbersdoubles()– generates aDoubleStreamof random numbers
Thread Safety of java.util.Random
java.util.Random is thread-safe, so you can safely call the methods of a single shared Random object from multiple threads.
Since the generated values are no true random numbers, but a random number is calculated from the previous one (this is very simplified; you can find more details in the chapter “Pseudorandom Numbers”) – and since this calculation must be done atomically, the invocation of the method is subject to a certain synchronization overhead.
Many simultaneous invocations from multiple threads can thus harm performance. In the GitHub repo, you can find the program RandomMultipleThreadsDemo, demonstrating the difference.
The following graph shows how long it takes on my 6-core i7 to generate 100 million random numbers via a shared Random instance: about one second for one thread, over 50 seconds for six parallel threads:

I have measured the extreme case, in which the threads continuously generate random numbers, i.e., constantly cause contention (conflicts when accessing the shared state). If a thread generates a random number only occasionally, contention occurs less frequently and may not be measurable at all.
Provided you expect thread contention and your application does not require the use of a single random number generator (which should only be the case in exceptional cases), you can create a separate Random object per thread. Then the time for 100 million random numbers remains at about one second, no matter how many threads are running in parallel (MultipleRandomMultipleThreadsDemo in GitHub):

Better still is to use ThreadLocalRandom described in the following section.
Java ThreadLocalRandom Class
In Java 7, the java.util.concurrent.ThreadLocalRandom class was introduced. The static method ThreadLocalRandom.current() provides a random number generator for each thread independent of all other threads. This way, no thread contention can occur, and the synchronization overhead described in the previous section is eliminated.
ThreadLocalRandom.current() returns a ThreadLocalRandom object, which in turn inherits from Random and thus provides the same methods, e.g., nextInt():
Random random = ThreadLocalRandom.current();
int randomNumber = random.nextInt();
The program ThreadLocalRandomMultipleThreadsDemo measures the performance of ThreadLocalRandom. The result: The generation of 100 million random numbers takes about 150 ms, no matter how many threads are running at the same time – an enormous performance gain compared to Random – not only with multiple threads but also with only one:

ThreadLocalRandom should, therefore, always be your first choice.
It is interesting to see how the implementation has changed fundamentally from Java 7 to Java 8. More about this in the section “Implementation of ThreadLocalRandom”.
Generate a Random Number in a Range
A common requirement is to generate a random number from a specific range. You have already seen a few possibilities:
Random.nextInt(int bound)– generates a random integer number between 0 and the specified upper bound.Random.ints(int randomNumberOrigin, int randomNumberBound)– generates an infinite stream of random numbers in the specified number range.Random.ints(long streamSize, int randomNumberOrigin, int randomNumberBound)– generates a stream of the specified number of random numbers in the specified number range.- the analogous stream-generating methods
Random.longs()andRandom.doubles().
The upper bound or randomNumberBound is always exclusive, i.e., the generated random numbers are always less than the specified maximum.
For a single random number with both a lower and an upper bound, Java 17 introduced the method nextInt(int origin, int bound) – on Random and all other generators, because it is defined in the RandomGenerator interface (more on that in the chapter “Random Number Generators Since Java 17”). ThreadLocalRandom has had this method since Java 7.
Here, too, the lower bound is inclusive and the upper bound is exclusive.
Random Number between 1 and 10
So you get a random number between 1 and 10 (both inclusive) with an upper bound of 11:
int number = ThreadLocalRandom.current().nextInt(1, 11);
Random Number between 1 and 100
Analogously, a random number between 1 and 100:
int number = ThreadLocalRandom.current().nextInt(1, 101);
Random Number between 1 and 6 (Rolling a Die)
And this is how you simulate the roll of a die:
int roll = ThreadLocalRandom.current().nextInt(1, 7);
Random Number between Two Values
For arbitrary bounds, you pass the lower bound as is and the upper bound plus one – so for the range from min to max inclusive:
int number = ThreadLocalRandom.current().nextInt(min, max + 1);
How to Generate a Random String in Java
So far, we have learned about methods of the Random class that allow us to create random integers (nextInt), random doubles (nextDouble), random arrays (nextBytes), and random booleans (nextBoolean).
However, sometimes we need a random string – that is, a string filled with a random or specific number of random characters.
We can do this by gradually filling a StringBuilder with random lowercase letters (you can find this and the following methods in the RandomUtils class in GitHub):
public static String randomLowerCaseString(int length) {
StringBuilder sb = new StringBuilder();
Random r = ThreadLocalRandom.current();
for (int i = 0; i < length; i++) {
char c = (char) ('a' + r.nextInt(26));
sb.append(c);
}
return sb.toString();
}
It gets a bit more complicated if we want to extend the character range, e.g., to upper case letters, numbers, and the space character. Then we have to define an alphabet and pick a random character from it:
private static final String ALPHANUMERIC_WITH_SPACE_ALPHABET =
" 0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
public static String randomAlphanumericalWithSpaceString(int length) {
return randomString(length, ALPHANUMERIC_WITH_SPACE_ALPHABET);
}
public static String randomString(int length, String alphabet) {
StringBuilder sb = new StringBuilder();
Random r = ThreadLocalRandom.current();
for (int i = 0; i < length; i++) {
int pos = r.nextInt(alphabet.length());
char c = alphabet.charAt(pos);
sb.append(c);
}
return sb.toString();
}
Since Java 8, we can also implement the randomString() method using a stream:
public static String randomStringWithStream(int length, String alphabet) {
return ThreadLocalRandom.current()
.ints(length, 0, alphabet.length())
.map(alphabet::charAt)
.collect(
StringBuilder::new, StringBuilder::appendCodePoint, StringBuilder::append)
.toString();
}
Alternatively, you can use the RandomStringUtils class from the open-source Apache Commons Lang project, e.g., RandomStringUtils.insecure().nextAlphanumeric(10). The static methods of this class are deprecated; since version 3.16, there are insecure() (based on ThreadLocalRandom) and secure() (based on SecureRandom) instead.
Pseudorandom Number Generation in Java
This article has mentioned the topic of “pseudorandom numbers” a few times already, and now it is time to take a closer look at the subject.
As briefly mentioned before, Random does not generate truly random numbers. It can’t because computers are based on algorithms, and algorithms are concrete rules that lead from a specific input to a particular output.
What is generated instead are so-called “pseudorandom numbers”. These are based on a sequence of numbers that starts at a specific value (the so-called “seed”) and where the next number is calculated from the previous one.
The algorithm is constructed in such a way that the generated numbers a) give the impression of being random and b) are evenly distributed in the number space (you can read the mathematical theory behind this in the Wikipedia article “Linear congruential generator”).
Seed Value
The starting value “seed” is generated from System.nanoTime() when the Random() constructor is called, XORed with a value that changes on every constructor call – so that two Random objects created in quick succession do not end up with the same seed. This makes it highly probable that the random number sequence will start at a different position each time a Random object is created.
But we can also set the seed value ourselves:
- in the constructor
Random(long seed) - or with the method
setSeed(long seed)
When we do that, we always get the same sequence of random numbers. We can check this quite easily: The following code (class SetSeedExample in GitHub) prints the sequence of numbers 30, 63, 48, 84, 70 twice every time it is started – on any operating system and with any Java version.
Random random = new Random(42);
for (int i = 0; i < 5; i++) {
System.out.println(random.nextInt(100));
}
random.setSeed(42);
for (int i = 0; i < 5; i++) {
System.out.println(random.nextInt(100));
}
Why doesn’t the number sequence start at 42?
You will find the answer in the next section.
Deriving Random Numbers from the Random Number Generator’s State
The “seed” value does not determine the first random number but the starting state of the random number generator. In the Random class, the state is stored in a 48-bit number calculated from the “seed”.
Inappropriately, the state is stored in a variable that also has the name seed; the name state would have been more appropriate. The respective subsequent state is calculated using the following formula:
seed = (seed * 25,214,903,917 + 11) & 248-1
From this 48-bit value, as many bits are then taken left-justified as are required for the requested random value, e.g., 32 bits for an int:

… or 1 bit for a boolean:

And for numbers that require more than 48 bits, like long or double?
- For a
long, 32 bits are picked, then the next state is calculated, then another 32 bits are picked. - For a
double, 26 bits are taken first and then another 27 bits, resulting in 53 bits (which corresponds to the precision of adoublevalue).
Never more than 32 bits are taken from the 48 bits. Why is the status then 48 bits long and not 32?
The reason is the following:
If the status were only 32 bits long, any given random integer would always be followed by the same successor. This, in turn, means that a single random number would be enough to predict all future random numbers.
Because the remaining 16 bits are hidden for a 48-bit status, there are 216 possible statuses for a 32-bit value, i.e., 65,536 different statuses in which the random number generator could currently be. So, the same random integer number can have 65,536 different successors, and this makes a prediction from a single number impossible.
However, if we know two consecutive random numbers, the situation is quite different. More about this in the section “Predictability of random numbers”.
Generating Pseudorandom Numbers in java.util.Random
What does this look like in java.util.Random’s source code?
Let’s start with the nextInt() method:
public int nextInt() {
return next(32);
}
The invoked next() method looks like this:
private static final long multiplier = 0x5DEECE66DL;
private static final long addend = 0xBL;
private static final long mask = (1L << 48) - 1;
// ...
synchronized protected int next(int bits) {
long nextseed = (seed * multiplier + addend) & mask;
seed = nextseed;
return (int)(nextseed >>> (48 - bits));
}
Line 8 implements the formula presented in the previous section for calculating the subsequent state.
In line 10, the requested bits are extracted by “right shift” and casting to an int. The following graphic illustrates this for 32 bits:

… and once for 1 bit:

The nextBoolean() method then only has to check whether the 1-bit number thus obtained is 1 or 0:
public boolean nextBoolean() {
return next(1) != 0;
}
The nextLong() method, as mentioned above, first gets 32 bits, shifts them 32 bits to the left, and adds a second 32-bit number:
public long nextLong() {
return ((long)(next(32)) << 32) + next(32);
}
The nextDouble() method is a bit more complicated (again, first the – somewhat easier to read, since not yet highly optimized – original implementation up to Java 7):
public double nextDouble() {
long l = ((long)(next(26)) << 27) + next(27);
return l / (double)(1L << 53);
}
The method performs the following two steps:
- Concatenation of a 26-bit random number with a 27-bit random number to form a 53-bit number (i.e., a number between 0 and 253-1).
- Division by 253 (one more than the highest number that could be generated in step 1).
This then results in a number >= 0.0 and < 1.0.
Why are only 53 bits used for a double and not 64? The reason: A double stores its fraction in 52 bits but has 53 significant bits. A normalized floating-point number has the form 1.xxx… · 2e – there is always a 1 before the binary point. Since that is true for every number, the format does not store this 1 at all, only the 52 bits after the point. Together with the always-present leading 1, that makes 53 bits of precision. The remaining twelve bits are the sign and the eleven-bit exponent. You can find a more detailed explanation in the Wikipedia article “Double-precision floating-point format”.
Similarly, the nextFloat() method uses 24 bits (23 stored fraction bits plus the leading 1) and not 32 (see Wikipedia article “Single-precision floating-point format”):
public float nextFloat() {
return next(24) / ((float)(1 << 24));
}
Predictability of Random Numbers
In the penultimate section, I claimed that we could predict from two consecutive pseudorandom numbers which subsequent numbers will follow. We can verify this assertion with a simple test.
The program RandomIntegerPairRepetitionFinder generates all 216 possible seed values for each of the 232 integer numbers – and from these their 216 potential successors. If the program finds the same pair of consecutive pseudorandom numbers more than once, it prints this to the console.
After 24 hours, no repetition was found on my laptop. However, the program would have to run for almost 1,200 hours, or 50 days, to check all combinations. I don’t have that much time, so I can’t prove my claim.
Nevertheless, we can write a program that predicts all further random numbers from two given ones. Such a program can detect if there is only one possible sequence for the input – and ask for a third number if two are not enough.
How to Predict the Next Random Number in Java
Given two random numbers generated by Random.nextInt(), predicting the subsequent random numbers is easy.
To do this, we determine the 216 possible seed values for the first given number, generate the respective successor for each and check whether it matches the second given number. If this is the case, we can calculate all subsequent random numbers based on the seed value determined in this way.
You can find the complete code for this in the RandomIntegerPredictor class.
The following code shows the method that determines the seed value. I have simplified it for printing in this article to use only two input numbers. The code in the linked class can also handle a longer input sequence.
private static final int NUMBER_OF_POSSIBLE_SEEDS = 1 << 16;
private static final long MULTIPLIER = 0x5DEECE66DL;
private static final long ADDEND = 0xBL;
private static final long MASK = (1L << 48) - 1;
// ...
private long getSeedMatchingForSequence() {
long firstNumberSeedBase = Integer.toUnsignedLong(givenNumbers[0]) << 16;
for (int noise = 0; noise < NUMBER_OF_POSSIBLE_SEEDS; noise++) {
long seed = firstNumberSeedBase | noise;
long nextSeed = getNextSeed(seed);
int nextInt = (int) (nextSeed >>> 16);
if (nextInt == givenNumbers[1]) {
return seed;
}
}
throw new IllegalArgumentException(
"Found no matching seed; please verify your input sequence.");
}
private long getNextSeed(long seed) {
return (seed * MULTIPLIER + ADDEND) & MASK;
}
From the seed value thus obtained, we can derive the further sequence of numbers:
public int[] predict(int numberOfPredictions) {
long seed = getSeedMatchingForSequence();
// Skip the given numbers
for (int i = 0; i < givenNumbers.length; i++) {
seed = getNextSeed(seed);
}
// Get the predictions
int[] predictions = new int[numberOfPredictions];
for (int i = 0; i < numberOfPredictions; i++) {
predictions[i] = (int) (seed >>> 16);
seed = getNextSeed(seed);
}
return predictions;
}
The GitHub repo contains a demo, RandomIntegerPredictorDemo, which prints two random numbers, predicts the following ten random numbers, and finally prints ten more random numbers. The prediction is correct:
Two random numbers:
-1,179,305,299
435,136,901
Predicting 10 random numbers:
-2,139,482,012
1,388,148,251
1,134,856,645
-1,205,820,716
182,240,689
...
Next *actual* random numbers:
-2,139,482,012
1,388,148,251
1,134,856,645
-1,205,820,716
182,240,689
...
The repo also contains the RandomIntegerPredictorRunner command line runner, which you can call (after compiling the code) as follows:
$ java eu.happycoders.random.predictor.RandomIntegerPredictorRunner 5 999571443 25208007
Given numbers: [999571443, 25208007]
Predicting 5 random numbers...
-1,315,941,039
136,476,741
1,077,533,899
-211,240,302
143,354,061
The first parameter, 5, specifies how many subsequent numbers to display; the following parameters are the known sequence of random numbers. The output is the prediction of the subsequent random numbers.
Java SecureRandom Class
In the last section of the previous chapter, you learned how to predict all subsequent random numbers from only two consecutive ones.
For certain requirements (e.g., cryptography), this predictability is not acceptable. In such cases, we need a “secure” random number generator.
The class java.security.SecureRandom has been available since Java 1.1. It generates “cryptographically strong” random numbers as defined in RFC 4086 “Randomness Requirements for Security”.
“Cryptographically strong” can be implemented in one of the following two ways (or a combination of both):
- A deterministic sequence like Random, but with much larger “noise” (the bits from the seed not included in the generated random number), so that reverse-engineering the seed from a given sequence becomes virtually impossible with today’s hardware.
- Truly random numbers generated by a special hardware that measures, for example, radioactive decay processes.
Different types of secure random number generators can be made available through the Java Service Provider Interface (SPI) and selected at runtime via SecureRandom.getInstance(String algorithm) and some overloaded variants of this method.
The constructor of SecureRandom provides a default implementation. SecureRandom inherits from Random and thus provides the same methods. The following example shows how you can generate a secure random floating-point number:
SecureRandom secureRandom = new SecureRandom();
double strongRandomNumber = secureRandom.nextDouble();
You can find more details in the SecureRandom JavaDoc.
Changes in Implementations over Time
In this section, you will learn how the random number generation methods presented in this article are implemented and how (and why) the implementations have been revised over the course of Java versions.
The following table summarizes which Java version changed what; the details follow in the sections below.
| Java version | Change |
|---|---|
| 1.0 / 1.1 | Math.random() and Random.next() are synchronized. |
| 1.3 | Math.random() switches to double-checked locking – and is no longer thread-safe. |
| 1.4 | Random.next() keeps its state in a sun.misc.AtomicLong, updated via compare-and-set. |
| 5 | Random.next() switches to java.util.concurrent.atomic.AtomicLong; SecureRandom.getAlgorithm() is added. |
| 6 | The AtomicLong field in Random becomes final. |
| 7 | ThreadLocalRandom is added – one instance per thread. |
| 8 | Math.random() switches to the initialization-on-demand holder pattern; Random.nextDouble() multiplies instead of dividing; ThreadLocalRandom is rewritten and stores its state in the Thread object; SecureRandom.getInstanceStrong() is added. |
| 9 | SecureRandom gets parameterizable DRBG generators with JEP 273. |
| 17 | All generators implement the new RandomGenerator interface; ThreadLocalRandom mixes the thread ID into the seed. |
| 19 | Random.from(RandomGenerator) is added; ThreadLocalRandom becomes final and uses threadId(). |
| 20 to 27 | No changes to the implementations shown here. |
Nothing has changed in Math.random() and Random since Java 8; everything after that concerns ThreadLocalRandom internals, SecureRandom parameters, and the shared interface.
Implementation of Math.random()
The static method Math.random() internally calls the nextDouble() method on a static instance of the Random class. The code for creating the static Random object has been changed several times.
In Java 1.0, the Math.random() method was simply provided with the synchronized keyword. That was correct but led to a high synchronization overhead:
public static synchronized double random() {
if (randomNumberGenerator == null)
randomNumberGenerator = new Random();
return randomNumberGenerator.nextDouble();
}
Java 1.3 switched to the “Double-checked locking” pattern:
// !!! DON'T DO THIS - NOT THREAD-SAFE !!!
public static double random() {
if (randomNumberGenerator == null) initRNG();
return randomNumberGenerator.nextDouble();
}
private static synchronized void initRNG() {
if (randomNumberGenerator == null)
randomNumberGenerator = new Random();
}
This supposedly more performant implementation is not thread-safe! Due to the unsynchronized read access to randomNumberGenerator in the random() method, a thread could see an incompletely initialized object at this point. You can find a detailed explanation in the article linked above.
It was not fixed until Java 8 – by switching to the “Initialization-on-Demand Holder” pattern:
private static final class RandomNumberGeneratorHolder {
static final Random randomNumberGenerator = new Random();
}
public static double random() {
return RandomNumberGeneratorHolder.randomNumberGenerator.nextDouble();
}
In this pattern, randomNumberGenerator is not created until it is needed; and the JVM guarantees proper synchronization when initializing the RandomNumberGeneratorHolder class.
There is no JEP for this change; it is tracked in the bug tracker under JDK-6470700 – the bug had been reported back in 2006.
This implementation has remained unchanged up to and including Java 27.
Implementation of java.util.Random
In the chapter “Pseudorandom Numbers in Java”, I explained the basic implementation of the Random class using its original source code. The public methods internally call the next(int bits) method.
Its synchronization was originally done via the synchronized keyword, which is easy to use, but – especially up to and including Java 5 – caused a high performance impact.
Here, once again, the original code (the next() method has existed since Java 1.1):
private long seed;
synchronized protected int next(int bits) {
long nextseed = (seed * multiplier + addend) & mask;
seed = nextseed;
return (int)(nextseed >>> (48 - bits));
}
In Java 1.4, the seed value was stored in a (then internal) sun.misc.AtomicLong and changed with attemptUpdate(). This method uses compare-and-set, i.e., optimistic locking:
import sun.misc.AtomicLong;
private AtomicLong seed;
protected int next(int bits) {
long oldseed, nextseed;
do {
oldseed = seed.get();
nextseed = (oldseed * multiplier + addend) & mask;
} while (!seed.attemptUpdate(oldseed, nextseed));
return (int)(nextseed >>> (48 - bits));
}
Java 5 moved to the new, official java.util.concurrent.atomic.AtomicLong and its compareAndSet() method:
import java.util.concurrent.atomic.AtomicLong;
private AtomicLong seed;
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));
}
In Java 6, the AtomicLong variable was made final. Up to and including Java 27, the implementation of the next() method has not been changed again.
Another change was made to the nextDouble() method. As a reminder – this is what it initially looked like:
public double nextDouble() {
long l = ((long)(next(26)) << 27) + next(27);
return l / (double)(1L << 53);
}
Since division is much more expensive than multiplication, in Java 8, the value 1/253 was extracted into a constant and is since used as a multiplication factor:
private static final double DOUBLE_UNIT = 0x1.0p-53; // 1.0 / (1L << 53)
public double nextDouble() {
return (((long)(next(26)) << 27) + next(27)) * DOUBLE_UNIT;
}
The functionality of the Random class has thus remained unchanged since Java 1.0. Up to Java 8, however, its performance has improved continuously.
Implementation of java.util.concurrent.ThreadLocalRandom
In Java 7, the ThreadLocalRandom.current() method returns a separate instance of ThreadLocalRandom per thread:
private static final ThreadLocal<ThreadLocalRandom> localRandom =
new ThreadLocal<ThreadLocalRandom>() {
protected ThreadLocalRandom initialValue() {
return new ThreadLocalRandom();
}
};
public static ThreadLocalRandom current() {
return localRandom.get();
}
ThreadLocalRandom inherits from Random, which stores the state in an AtomicLong. Since its thread safety (and the resulting overhead) are not needed in ThreadLocalRandom, ThreadLocalRandom keeps the state in its own field rnd instead, which it accesses without synchronization:

In Java 8, ThreadLocalRandom has been completely rewritten. Instead of providing separate instances per thread, ThreadLocalRandom.current() returns the same static instance of ThreadLocalRandom for all threads.
To prevent all threads from accessing the same state (which would then have to be synchronized again), the state is stored in the threadLocalRandomSeed variable of the current thread’s Thread object:

There is no JEP for this change; it is tracked in the bug tracker under JDK-8005926.
ThreadLocalRandom accesses this field via Unsafe – sun.misc.Unsafe in Java 8, jdk.internal.misc.Unsafe since Java 9. This is what the code looks like in Java 27:
private static final Unsafe U = Unsafe.getUnsafe();
private static final long SEED
= U.objectFieldOffset(Thread.class, "threadLocalRandomSeed");
// ...
final long nextSeed() {
Thread t; long r; // read and update per-thread seed
U.putLong(t = Thread.currentThread(), SEED,
r = U.getLong(t, SEED) + (t.threadId() << 1) + GOLDEN_GAMMA);
return r;
}
The formula has changed twice along the way: In Java 8 to 16, only the constant GAMMA was added. Since Java 17, the thread ID is mixed in as well – initially via getId(), since Java 19 via threadId(), because getId() has been deprecated since then.
You can easily show that different threads use different ThreadLocalRandom instances in Java 7 and the same ThreadLocalRandom instance in Java 8 with the following code:
for (int i = 0; i < 3; i++) {
new Thread() {
@Override
public void run() {
System.out.println(
"ThreadLocalRandom instance: " + ThreadLocalRandom.current());
}
}.start();
}
If you run the code under Java 7, you will see three different instances; under Java 8, you will see the same instance three times.
Implementation of java.security.SecureRandom
SecureRandom is not itself an implementation of a secure random number generator. Concrete implementations are provided via the Service Provider Interface (SPI). Going into these is beyond the scope of this article.
Three enhancements to SecureRandom itself are worth mentioning:
- In Java 1.5, the
getAlgorithm()method was added, returning the name of the implemented algorithm. - In Java 8, the static
getInstanceStrong()method was added, returning the implementation of a particularly strong algorithm, such as those recommended for RSA key pairs. - In Java 9, JEP 273 (“DRBG-Based SecureRandom Implementations”) added the
SecureRandomParametersinterface and methods built on it:getInstance(String algorithm, SecureRandomParameters params),getParameters(),nextBytes(byte[] bytes, SecureRandomParameters params), andreseed().
Random Number Generators Since Java 17: RandomGenerator
Up to Java 16, SplittableRandom had almost the same methods as Random, ThreadLocalRandom, and SecureRandom, but no common type with them. If you wanted to switch between the generators, you had to touch the code.
Java 17 changed that with JEP 356 (“Enhanced Pseudo-Random Number Generators”): All four classes have since implemented the java.util.random.RandomGenerator interface, which defines the shared methods such as nextInt(), nextDouble(), and ints(). I recommend using this type in your own code – then you swap the algorithm in exactly one place.
The interface comes with methods that Random did not have before:
nextInt(int origin, int bound),nextLong(long origin, long bound),nextFloat(float origin, float bound), andnextDouble(double origin, double bound)– random number with lower and upper bounds,nextLong(long bound),nextFloat(float bound), andnextDouble(double bound)– random number with an upper bound,nextGaussian(double mean, double stddev)– normally distributed random number with a mean and standard deviation of your choice,nextExponential()– exponentially distributed random number.
In addition, there are new algorithms of the so-called “LXM” family. The name stands for its three building blocks: a linear congruential generator (L) like the one in Random, a xor-based generator (X) from the xorshift family, and a mixing function (M) that combines the results of both. The numbers in the name are the state bits of the two subgenerators – so L64X128MixRandom has a 64-bit congruential generator and a 128-bit xor generator (details in the java.util.random package description).
You select an algorithm by its name:
RandomGenerator generator = RandomGenerator.of("L64X128MixRandom");
int roll = generator.nextInt(1, 7);
If you don’t care which algorithm you get, use RandomGenerator.getDefault() – in Java 17 to 27, this method returns an L32X64MixRandom. According to the JavaDoc, however, there is explicitly no guarantee that this stays the same: With a new Java version, getDefault() may return a different algorithm. If you have to rely on the algorithm, call it by name.
Two smaller additions came later:
- Since Java 19,
Random.from(RandomGenerator)turns any generator into aRandomobject – for APIs that require that type (JDK-8279598). - Since Java 19,
ThreadLocalRandomisfinal(JDK-8283688) – there never were any subclasses anyway, as the constructor is private.
You can find the complete interface hierarchy (StreamableGenerator, SplittableGenerator, JumpableGenerator, and more), including class diagrams before and after Java 17, in the article about Java 17.
Summary
This article started by showing the various ways to generate random numbers in Java.
After that, you learned why random numbers are actually pseudorandom numbers, how to calculate a sequence of random numbers, and how to predict all future random numbers from two given random numbers.
The article concluded with a journey through Java versions and in it showed how (and why) the implementations of the previously shown methods have changed over time.
My recommendation from all this: ThreadLocalRandom for everything that merely has to look random; SecureRandom for everything an attacker must not be able to guess – tokens, passwords, keys; and, since Java 17, RandomGenerator as the type in your own code, so that the algorithm remains interchangeable.
If you enjoyed this article, I’d appreciate a review on my ProvenExpert profile.
And if you want to stay up to date on new Java features, click here and sign up for the HappyCoders newsletter.




