ThreadLocalRandom
The Java ThreadLocalRandom class is a newer subclass of the Java Random class
that is most suitable for use under the following circumstances:
- you need a "medium quality" fast, non-cryptographic random nubmer generator;
- you do not require a specific algorithm or seed;
- each use of the generator by a given thread will be for an independent task;
- if multiple threads/processes are working together on the same task, there will be a moderate number
of threads or processes.
Conversely:
- ThreadLocalRandom is not suitable if you require any level of security;
- ThreadLocalRandom may not be suitable for certain "divide and conquer" tasks
where a large number of threads could be working together on the same task and/or where a huge
number of random numbers need to be consumed;
- ThreadLocalRandom is not suitable when you require a specific algorithm or seed.
Why use ThreadLocalRandom?
Despite the above limitations, ThreadLocalRandom does offer an advantage over java.util.Random:
in practice (as of Java 8), it uses a higher-quality random number generation algorithm
that passes many statistical tests, and has a larger period of 264.
As well as being higher quality, ThreadLocalRandom is generally several times faster
than java.lang.Random.
How to use ThreadLocalRandom
The ThreadLocalRandom class can be used essentially as a drop-in replacement for the standard
java.util.Random class, except that you do not explicitly create
an instance of the class. Instead, you use the static ThreadLocalRandom.current() method to
request a predetermined instance. From then on, you can call the usual Random methods on this
instance, such as nextInt(), nextDouble() etc. For example, the following code
simulates rolling two dice using ThreadLocalRandom:
Random r = ThreadLocalRandom.current();
int dice1 = r.nextInt(6) + 1;
int dice2 = r.nextInt(6) + 1;
Note that this means you cannot specify a seed: in effect, you "get what you're given"!
Explanation of the ThreadLocalRandom algorithm
The ThreadLocalRandom uses an algorithm called SplitMix1.
SplitMix works by taking a simple seed sequence and, for each number in the sequence, applies a mixing function
(effectively a kind of "hash") to each number. The output from that hash function becomes the next random number.
For the time being, we can imagine that the seed sequence could be a simple counting sequence (1, 2, 3...);
we will come back to the actual sequence used by ThreadLocalRandom shortly.
Most of the "magic" to turn this sequence into a sequence of "random" numbers is therefore in the mixing function.
The ThreadLocalRandom mixing function
The mixing function has essentially the same goals as a hash function:
- avalanching: if two numbers in our input sequence differ by just a single bit,
then we want that tiny difference to "avalanche"— in other words, cause each output bit to have ~50% chance of being different;
- uniform distribution: outputs should be evenly distributed over the possible values.
The SplitMix mixing function used by ThreadLocalRandom is actually derived from the mixing function of the 64-bit
variant of MurmurHash3. It looks as follows:
private static long mix64(long z) {
z = (z ^ (z >>> 33)) * 0xff51afd7ed558ccdL;
z = (z ^ (z >>> 33)) * 0xc4ceb9fe1a85ec53L;
return z ^ (z >>> 33);
}
The mixing function in essence contains two multiplicative LCGs. An MLCG is essentially a degenerate form
of an LCG or linear congruential generator, where we multiply by a "magic" constant that is observed to
give good randomness, but do not add any constant, and take the size of the primitive itself (in this case,
a Java long = 264) as the modulus. On its own, a single MLCG makes a poor generator, and
as with LCGs in general, the bottom bits exhibit much less randomness than the top bits. But in the mixer function,
we use two MCLGs, combined with shifts and XOR operations to mix the higher bits in with the lower bits to
"distribute" the randommess across the entire long. (This is a little similar to the secondary
hash function you may have seen in the implementation of the Java HashMap class.)
Further XORs, shifts and MLCGs may well improve the quality of the output. But in practice, the MurmurHash3 authors
found that two multiplications alone with these constants produced good results. The authors of Java's SplitMix
algorithm found that combining this mixing function with a simple underlying sequence (see below) produced a
random number generator that passes various standard batteries of statistical tests.
As with many of the constants used in random number generators, the two magic constants 0xff51afd7ed558ccdL
and 0xc4ceb9fe1a85ec53L were essentially found empirically.
The seed sequence and gamma
In principle, we could produce a random number generator from the above mix64() method as follows:
- start with a 64-bit seed;
- each time we want to generate a new 64-bit random number, call mix64() on the current seed
and take the output;
- then, increment the seed by 1 (or some other odd-valued constant) before taking the next random value.
Ensuring that we increment the seed by an odd number each time ensures that we would eventually
go through all 264 possible seed values: in other words, the period of the generator will be
264. (If we incremented by 2 each time, for example, we would skip half of the possible
values.)
But in practice, we can try to choose an increment that itself helps to provide some
extra randomness. Intuitively, we would like to use an (odd-valued) increment that:
- has no particular 'regularity' or repeating patterns in its binary digits;
- (thereby) has roughly half its bits set.
The chosen increment is termed the gamma. For ThreadLocalRandom, all threads use the
same gamma. The value taken is the so-called golden gamma, 0x9e3779b97f4a7c15L:
a 64-bit representation of the golden ratio.
In other words, it is an irrational number. (A sequence of nubmers constructed by adding
an irrational number in this way is sometimes referred to as a Wehl sequence.)
The starting seed for ThreadLocalRandom
All threads use the same gamma, yet clearly if we have multiple threads each using
ThreadLocalRandom, the whole point is that we want each thread to have its own "unpredictable" sequence
of numbers.
We achieve this, as you might expect, by giving each thread a different starting seed.
The first thread can seed the generator on the current time, as you might expect.
(Depending on configuration, the current implementation allows either a combination of System.currentTimeMillis() and
System.nanoTime(), or a seed dervied from sources of entropy via SecureRandom.)
Subsequent threads then take a starting seed itself derived by applying a mixing function to the
previous starting seed, etc, so that we hope that overall, the different threads will have
starting seeds that are well distributed over the possible range of seeds. This is generally good enough for common situations
when multiple threads are performing independent tasks requiring sequences of random
numbers.
But of course, the multiple threads will ulimately be taking their random nubmers from different portions
of the same overarching sequence of 264 numbers (just as they would if java.lang.Random
or any other standard generator was used). Where a large number of threads or processes will work together on a joint
task, the SplittableRandom class may be more appropriate.
Nonetheless, for many applications, ThreadLocalRandom is probably the default generator to use unless you
specifically require an alternative for any of the reasons outlined above.
What to read next
See:
Notes:
1. The first implementation of ThreadLocalRandom in Java 7 used the same LCG algorithm as
java.util.Random.
If you enjoy this Java programming article, please share with friends and colleagues. Follow the author on Twitter for the latest news and rants.
Editorial page content written by Neil Coffey. Copyright © Javamex UK 2021. All rights reserved.