Java JUC ThreadLocalRandom class parsing

Posted by kasitzboym on Wed, 05 Jan 2022 09:27:28 +0100

ThreadLocalRandom class parsing

preface

ThreadLocalRandom class is a new Random number generator added by JDK7 under JUC package. It mainly solves the shortage of Random class under multithreading.

This article explains the principle of the random thread class and why it needs to be implemented.

Random class and its limitations

First, let's look at the random class. From before JDK7 to now, Java util. Random classes are widely used random number generation tool classes, and Java Lang. math's random number generation also uses Java util. For an example of the random class, let's take a look at how to use the random class.

public static void main(String[] args) {
        //1. Create a default seed random number generator
        Random random = new Random();
        //2. Output 10 random numbers between 0 and 5 (including 0 and excluding 5)
        for (int i = 0; i < 10; i++) {
            System.out.print(random.nextInt(5)); //3421123432
        }
}

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

The seed number is only the starting number of the random algorithm and has nothing to do with the interval of the generated random number.

public Random() {
        this(seedUniquifier() ^ System.nanoTime());
}
public Random(long seed) {
        if (getClass() == Random.class)
            this.seed = new AtomicLong(initialScramble(seed));
        else {
            // subclass might have overriden setSeed
            this.seed = new AtomicLong();
            setSeed(seed);
        }
}

After having the default seed, how does Random generate Random numbers? Let's look at the nextInt() method.

public int nextInt(int bound) {
              //3. Parameter inspection
        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:

  • Generate new seeds from old seeds
  • Then calculate a new random number according to the new seed

Step 4 can be abstracted as seed = f(seed), for example, seed = f(seed) = a*seed+b;

Step 5 can be abstracted as g(seed,bound), such as g(seed,bound) = (int) ((bound * (long) seed) > > 31);

In the case of single thread, each call to nextInt() calculates a new seed based on the old seed, which can ensure the randomness of random numbers. However, under multithreading, multiple threads may take the same old seed to execute step 4 to calculate the new seed, which will cause the new seed of multiple threads to be the same, and because the algorithm in step 5 is fixed, it will cause multiple threads to produce the same random value.

Therefore, step 4 should 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 should discard its old seed and use the new seed of the first thread to calculate its new seed, and so on.

💡 The atomic variable AtomicLong is used in Random to achieve this effect. The seed initialized when creating the Random object is saved in the seed atomic variable.

Next, take a look at the next() method:

protected int next(int bits) {
        long oldseed, nextseed;
        AtomicLong seed = this.seed;
        do {
            //6. Get current variable seed
            oldseed = seed.get();
            //7. Calculate a new seed based on the current seed variable
            nextseed = (oldseed * multiplier + addend) & mask;
          //8. Using CAS operation, it updates the old seed with the new seed. The failed thread will re obtain the updated seed as the current seed to calculate the old seed
        } while (!seed.compareAndSet(oldseed, nextseed));
        return (int)(nextseed >>> (48 - bits));
}

Summary: in multithreading, although only one thread will succeed at the same time, it will cause a large number of threads to spin, which will reduce the concurrency performance. Therefore, ThreadLocalRandom is used.

ThreadLocalRandom

In order to make up for the efficiency of Random under multithreading, the ThreadLocalRandom class is added to the JUC package. The following is a demonstration of how to use ThreadLocalRandom class:

public static void main(String[] args) {
        //1. Get a random number generator
        ThreadLocalRandom random = ThreadLocalRandom.current();
        //2. Output 10 random numbers between 0-5 (including 0 and excluding 5)
        for (int i = 0; i < 10; ++i) {
            System.out.print(random.nextInt(5));
        }

}

The first step calls threadlocalrandom Current() to get the random number of the current thread.

In fact, the implementation of ThreadLocalRandom is similar to ThreadLocal. Each thread copies a variable, so that each thread actually operates a copy in local memory, avoiding the synchronization of shared variables.

The disadvantage of Random is that multiple threads will use the same atomic seed variable, resulting in competition for atomic variable updates.

In ThreadLocalRandom, each thread maintains a seed variable. When each thread generates a random number, it calculates a new seed according to the old seed in the current thread, updates the old seed with the new seed, and then calculates the random number according to the new seed, so there will be no competition problem.

Source code analysis

First, let's take a look at the class diagram structure of ThreadLocalRandom.

As can be seen from the figure, ThreadLocalRandom inherits the Random class and rewrites the nextInt() method. The atomic seed variable of Random is not used in the ThreadLocalRandom class.

There is no specific seed stored in ThreadLocalRandom. The specific seed is placed in the ThreadLocalRandomSeed variable of the specific calling thread.

ThreadLocalRandom is similar to ThreadLocal class and 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 to initialize the seed.

When calling the nextInt() method of ThreadLocalRandom, it is actually to obtain the ThreadLocalRandomSeed variable of the current thread as the current seed to calculate the new seed, then update the new seed to ThreadLocalRandomSeed, and then calculate the random number according to the new seed.

It should be noted that the threadLocalRandomSeed variable is an ordinary long variable in the Thread class, not an atomic variable.

@sun.misc.Contended("tlr")
long threadLocalRandomSeed;

Because this variable is thread level, there is no need to use atomic type variables at all.

Seed and probeGenerator in ThreadLocalRandom are two atomic variables. The purpose of declaring probeGenerator and seed as atomic variables is to give them different seed initial values in the case of multiple threads, so that the random number sequence generated by each thread will not be the same, Moreover, probeGenerator and seeder will only be used when initializing the seed and probe variables of the calling thread (used for decentralized calculation of array index subscripts), and each thread will only use them once.

In addition, the variable instance is an instance of threadlocalrandom. The variable is static. Multiple threads use the same instance. However, because the specific seed exists in the thread, the threadlocalrandom instance contains only general algorithms independent of the thread, so it is thread safe.

Let's take a look at the main code logic of ThreadLocalRandom class:

1.Unsafe mechanism

    // Unsafe mechanics
    private static final sun.misc.Unsafe UNSAFE;
    private static final long SEED;
    private static final long PROBE;
    private static final long SECONDARY;
    static {
        try {
              //Get instance
            UNSAFE = sun.misc.Unsafe.getUnsafe();
            Class<?> tk = Thread.class;
            //Gets the offset of the threadLocalRandomSeed variable in the Thread class in the Thread instance
            SEED = UNSAFE.objectFieldOffset
                (tk.getDeclaredField("threadLocalRandomSeed"));
            //Gets the offset of the threadLocalRandomProbe variable in the Thread class in the Thread instance
            PROBE = UNSAFE.objectFieldOffset
                (tk.getDeclaredField("threadLocalRandomProbe"));
            //Gets the offset of the threadLocalRandomSecondarySeed variable in the Thread class in the Thread instance
            SECONDARY = UNSAFE.objectFieldOffset
                (tk.getDeclaredField("threadLocalRandomSecondarySeed"));
        } catch (Exception e) {
            throw new Error(e);
        }
    }

2.ThreadLocalRandom current() method

This method obtains the ThreadLocalRandom instance and initializes the threadLocalRandomSeed and threadLocalRandomProbe variables in the calling thread.

static final ThreadLocalRandom instance = new ThreadLocalRandom();
public static ThreadLocalRandom current() {
        if (UNSAFE.getInt(Thread.currentThread(), PROBE) == 0)
            localInit();
        return instance;
}
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, if the variable value of threadLocalRandomProbe in the current thread is 0 (by default, this variable of the thread is 0), it means that the current thread calls the current() method 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.

In localInit(), first calculate the threadLocalRandomProbe initialization value in the current thread according to the probeGenerator, then calculate the initialization seed of the current thread according to the seeder, and then set these two variables to the current thread.

An instance of ThreadLocalRandom is returned at the end of current. It should be noted that this method is static, and multiple threads return the same ThreadLocalRandom instance.

3.int nextInt(int bound) method

Calculates the next random number for the current thread.

public int nextInt(int bound) {
        //Parameter verification
        if (bound <= 0)
            throw new IllegalArgumentException(BadBound);
        //Calculates a new seed based on the seed in the current thread
        int r = mix32(nextSeed());
        //Calculate random number based on new seed
        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 above logic is similar to Random. The focus is on the nextSeed() method, which is mainly used to obtain and update their seed variables and generate Random numbers.

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;
}

In this code, first use the variable r = UNSAFE After getlong (T, seed), get the value of the threadLocalRandomSeed variable in the current thread, and then accumulate the GAMMA value as a new seed based on the seed. Then, use the UNSAFE putLong method to put the new seed into the threadLocalRandomSeed variable of the current thread.

4.long initialSeed() method

private static final AtomicLong seeder = new AtomicLong(initialSeed());

    private static long initialSeed() {
        String sec = VM.getSavedProperty("java.util.secureRandomSeed");
        if (Boolean.parseBoolean(sec)) {
            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);
            return s;
        }
        return (mix64(System.currentTimeMillis()) ^
                mix64(System.nanoTime()));
}

When initializing the atomic variable seeder corresponding to the initial value of the seed variable, the initialSeed() method is called to judge Java util. Whether the system attribute value of securerandomseed is true determines whether to use the seed with high security. If it is true, Java security. SecureRandom. Getseed (8) obtains high security seeds. If it is false, the initialization seeds are obtained according to the current timestamp, that is, the use of high security seeds cannot be predicted, while Random and ThreadLocalRandom are called "pseudo-Random numbers" because they can be predicted.

summary

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.

Topics: JUC