Java Review - Concurrent Programming_ Implementation principle of ThreadLocalRandom & source code analysis

Article catalog

summary

ThreadLocalRandom class is a new Random number generator added by JDK 7 under the JUC package. It makes up for the defect of Random class under multithreading. Here we mainly explain why this class should be added under JUC and the implementation principle of this class.

Limitations of Random

Before JDK 7 and now, java.util.Random is a widely used random number generation tool class, and the random number generation in java.lang.Math also uses the instance of java.util.Random.

import java.util.Random;

/**
 * @author Small craftsman
 * @version 1.0
 * @description: TODO
 * @date 2021/11/28 23:05
 * @mark: show me the code , change the world
 */
public class RandomTest {

    public static void main(String[] args) {
        // 1
        Random random = new Random();
        for (int i = 0; i < 10; i++) {
            // 2
            System.out.println(random.nextInt(5));
        }

    }
}
  • Code (1) creates a default random number generator and uses the default seed.
  • Code (2) outputs 10 random numbers between 0 and 5 (including 0 and excluding 5).

The generation of Random numbers requires a default seed, which is actually a long type number. You can specify it through the constructor when creating Random objects. If not, a default value will be generated inside the default constructor.

With the default seed, how to generate random numbers?

public int nextInt(int bound) {
         // 3 parameter check
        if (bound <= 0)
            throw new IllegalArgumentException(BadBound);
        // 4 generate new seeds from old seeds 
        int r = next(31);
       // 5 calculate the random number according to the new seed
        int m = bound - 1;
        if ((bound & m) == 0)  // i.e., bound is a power of 2
            r = (int)((bound * (long)r) >> 31);
        else {
            for (int u = r;
                 u - (r = u % bound) + m < 0;
                 u = next(31))
                ;
        }
        return r;
    }

It can be seen that the generation of new random numbers requires two steps:

  • First, new seeds are generated from old seeds.
  • Then a new random number is calculated according to the new seed.
  • Step (4) we can abstract it as seed=f(seed), where f is a fixed function, such as seed=f(seed)=a*seed+b;
  • Step (5) can also be abstracted as g(seed, bound), where g is a fixed function, such as g(seed, bound) = (int) ((bound * (long) seed) > > 31).

In the case of single thread, each call to nextInt calculates a new seed according to the old seed, which can ensure the randomness of random numbers.

However, in multithreading, multiple threads may take the same old seed to execute step (4) to calculate the new seed, which will lead to the same new seed generated by multiple threads. Since the algorithm in step (5) is fixed, multiple threads will produce the same random value, which is not what we want.

Step (4) ensure atomicity, that is, when multiple threads calculate new seeds according to the same old seed, after the new seed of the first thread is calculated, the second thread will discard its old seed and use the new seed of the first thread to calculate its new seed. By analogy, this is the only guarantee, In order to ensure that the random number generated under multithreading is random.

The Random function uses an atomic variable to achieve this effect. The seed initialized when creating the Random object is saved in the seed atomic variable. See the code of next().

    protected int next(int bits) {
        long oldseed, nextseed;
        AtomicLong seed = this.seed;
        do {
        	// 6 
            oldseed = seed.get();
            // 7 
            nextseed = (oldseed * multiplier + addend) & mask;
            // 8
        } while (!seed.compareAndSet(oldseed, nextseed));
        // 9 
        return (int)(nextseed >>> (48 - bits));
    }
  • Code (6) gets the value of the current atomic variable seed.
  • Code (7) calculates a new seed based on the current seed value.
  • Code (8) uses CAS operation, which uses new seeds to update the old seeds. Under multithreading, multiple threads may execute code (6) at the same time, so multiple threads may get the same current seed value, and then the new seeds calculated in step (7) are the same, but step (8) The CAS operation of ensures that only one thread can update the old seed to a new one, and the failed thread will re obtain the updated seed as the current seed to calculate the old seed through a cycle, which solves the problems mentioned above and ensures the randomness of random numbers.
  • Code (9) calculates a random number from the new seed using a fixed algorithm.

Summary: each Random instance has an atomic seed variable to record the current seed value. When a new Random number is to be generated, the new seed needs to be calculated according to the current seed and updated back to the atomic variable. When using a single Random instance to generate Random numbers in multi threads, when multiple threads calculate Random numbers to calculate new seeds at the same time, multiple threads will compete for the update operation of the same atomic variable. Because the update of atomic variables is a CAS operation and only one thread will succeed, a large number of threads will spin retry, which will reduce the concurrency performance, So ThreadLocalRandom came into being.

Application and principle of ThreadLocalRandom

In order to make up for the defect of Random in the case of multithreading and high concurrency, a ThreadLocalRandom class is added under the JUC package

use

import java.util.concurrent.ThreadLocalRandom;

/**
 * @author Small craftsman
 * @version 1.0
 * @description: TODO
 * @date 2021/11/28 23:28
 * @mark: show me the code , change the world
 */
public class ThreadLocalRandomTest {

    public static void main(String[] args) {
        // 10 get a random number generator
        ThreadLocalRandom tr = ThreadLocalRandom.current();

        // 11 output 10 random numbers (including 0 and excluding 5)
        for (int i = 0; i < 10; i++) {
            System.out.println(tr.nextInt(5));
        }
    }
}
  • Code (10) calls ThreadLocalRandom.current() to get the random number generator of the current thread

principle

From the name, it reminds us of ThreadLocal: ThreadLocal allows each thread to copy a variable, so that when each thread operates on the variable, it actually operates the copy in its own local memory, thus avoiding the synchronization of shared variables.

In fact, the implementation of ThreadLocalRandom is also based on this principle. The disadvantage of Random is that multiple threads will use the same atomic seed variable, resulting in competition for atomic variable update

Then, if each thread maintains a seed variable, when each thread generates a random number, it calculates a new seed according to its old seed, updates the old seed with the new seed, and then calculates the random number according to the new seed, there will be no competition problem, which will greatly improve the concurrency performance.

ThreadLocalRandom source code analysis

  • The ThreadLocalRandom class inherits the Random class and rewrites the nextInt method. The atomic seed variable inherited from the Random class is not used in the ThreadLocalRandom class.
  • There is no specific seed stored in ThreadLocalRandom. The specific seed is stored in the threadLocalRandomSeed variable of the specific calling thread.
  • ThreadLocalRandom is similar to ThreadLocal class, which is a tool class. When a thread calls the current method of ThreadLocalRandom, ThreadLocalRandom is responsible for initializing the threadLocalRandomSeed variable of the calling thread, that is, initializing the seed.
  • When the nextInt method of ThreadLocalRandom is called, it actually obtains the threadLocalRandomSeed variable of the current thread as the current seed to calculate the new seed, then updates the new seed to the threadLocalRandomSeed variable of the current thread, and then calculates the random number according to the new seed and using a specific algorithm. It should be noted here that the threadLocalRandomSeed variable is an ordinary long variable in the Thread class, which is not an atomic variable. In fact, the reason is very simple. Because this variable is Thread level, you don't need to use atomic variables at all. If you still don't understand it, you can think about the principle of ThreadLocal.

seeder and probeGenerator are two atomic variables, which will be used when initializing the seed and probe variables of the calling thread. Each thread will only use them once.

In addition, the variable instance is an instance of ThreadLocalRandom, which is static. When a multithread obtains an instance of ThreadLocalRandom through the current method of ThreadLocalRandom, it actually obtains the same instance. However, since the specific seed is stored in the thread, the ThreadLocalRandom instance only contains general algorithms that are not related to the thread, so it is thread safe

ThreadLocalRandom current()

Method to obtain the ThreadLocalRandom instance, and initialize the threadLocalRandomSeed and threadLocalRandomProbe variables in the calling thread.

    /**
     * Returns the current thread's {@code ThreadLocalRandom}.
     *
     * @return the current thread's {@code ThreadLocalRandom}
     */
    public static ThreadLocalRandom current() {
         // 12 
        if (UNSAFE.getInt(Thread.currentThread(), PROBE) == 0)
            // 13 
            localInit();
            // 14
        return instance;
    }
   /**
     * Initialize Thread fields for the current thread.  Called only
     * when Thread.threadLocalRandomProbe is zero, indicating that a
     * thread local seed value needs to be generated. Note that even
     * though the initialization is purely thread-local, we need to
     * rely on (static) atomic generators to initialize the values.
     */
    static final void localInit() {
        int p = probeGenerator.addAndGet(PROBE_INCREMENT);
        int probe = (p == 0) ? 1 : p; // skip 0
        long seed = mix64(seeder.getAndAdd(SEEDER_INCREMENT));
        Thread t = Thread.currentThread();
        UNSAFE.putLong(t, SEED, seed);
        UNSAFE.putInt(t, PROBE, probe);
    }

In the above code (12), if the variable value of threadLocalRandomProbe in the current Thread is 0 (by default, the variable value of the Thread is 0), it indicates that the current Thread calls the current method of ThreadLocalRandom for the first time, so you need to call the localInit method to calculate the initialization seed variable of the current Thread. In order to delay initialization, the seed variable in the Thread class is not initialized when the random number function is not required. This is an optimization.

Code (13) first calculates the initialization value of threadLocalRandomProbe in the current thread according to probeGenerator, then calculates the initialization seed of the current thread according to seeder, and then sets these two variables to the current thread. Code (14) returns an instance of ThreadLocalRandom. It should be noted that this method is static, and multiple threads return the same ThreadLocalRandom instance.

int nextInt(int bound)

    public int nextInt(int bound) {
       // 15  
        if (bound <= 0)
            throw new IllegalArgumentException(BadBound);
        // 16 
        int r = mix32(nextSeed());
        // 17 
        int m = bound - 1;
        if ((bound & m) == 0) // power of two
            r &= m;
        else { // reject over-represented candidates
            for (int u = r >>> 1;
                 u + m - (r = u % bound) < 0;
                 u = mix32(nextSeed()) >>> 1)
                ;
        }
        return r;
    }

The logical steps of the above code are similar to Random. Let's focus on the nextSeed() method.

    final long nextSeed() {
        Thread t; long r; // read and update per-thread seed
        UNSAFE.putLong(t = Thread.currentThread(), SEED,
                       r = UNSAFE.getLong(t, SEED) + GAMMA);
        return r;
    }

Firstly, use r = UNSAFE.getLong(t, SEED) to obtain the value of threadLocalRandomSeed variable in the current thread, then accumulate the GAMMA value based on the seed as the new seed, and then use UNSAFE's putLong method to put the new seed into the threadLocalRandomSeed variable of the current thread.

Summary

Here, we mainly describe the implementation principle of Random and the disadvantage that Random needs to compete for seed atomic variable update operation under multithreading, so as to lead to the ThreadLocalRandom class.

ThreadLocalRandom uses the principle of ThreadLocal to make each thread hold a local seed variable, which can only be initialized when using random numbers.

When calculating new seeds in multithreading, they are updated according to the seed variables maintained in their own threads, so as to avoid competition.

Posted by orange08 on Sun, 28 Nov 2021 20:37:29 -0800