并发编程第五天----ThreadLocalRandom源码解析

1,160 阅读5分钟

Random

Random 类在我们开发中很常用,可以生成随机数,我们来分析一下他的执行原理以及缺陷。


使用

随机数的生成需要一个种子(一个 long 类型的变量),并根据种子通过某种固定算法生成随机数。

public class RandomTest {
    public static void main(String[] args) {
        for (int i = 0; i < 5; i++) {
            Random random = new Random(111);
            System.out.println(random.nextInt(100));
        }
    }
}

当种子固定时,生成的随机数也一样


nextInt源码分析

public int nextInt(int bound) {
    if (bound <= 0)
        throw new IllegalArgumentException(BadBound); //参数检验
	
    // 根据旧的 seed 升成新的 seed ,并生成随机数 r
    int r = next(31); 
    
    
    // 固定算法,对 r 进行加工操作
 	...
        
    return r;
}

// 用原子变量保存种子的值
private final AtomicLong seed;

// 根据旧的 seed 升成新的 seed ,并生成随机数 r
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)); //CAS更新操作,保证多线程环境下产生的随机数是随机的
    
    // 使用固定算法,根据新的种子值生成随机数
    return (int)(nextseed >>> (48 - bits));
}

随机数的生成可以抽象成两步:

  • 根据老的种子生成新的种子
  • 根据新的总子生成新的随机数

在单线程情况下每次调用 nextInt() 都是根据老的种子计算出新的种子,可以保证随机数产生的随机性。多线程环境下,多个线程可能都拿到同一个老的种子去执行 next() 方法,由于生成随机数的算法是固定的,所以会导致多个线程产生相同的随机数。

为了保证多线程环境下产生的随机数是随机的,将种子值保存到了原子变量里,并通过 CAS 操作更新种子,保证了只有一个线程可以更新老的种子为新的,保证了生成的随机数的随机性。


总结&缺陷

总结: 每个 Random 实例里面都有一个原子性的种子变量 private final AtomicLong seed; 用来记录当前种子值,当要生成新的随机数时需要根据当前种子计算新的种子并更新回原子变量。

缺陷: 高并发情况下,大量线程同时竞争一个原子变量的更新,严重影响 CPU 性能,ThreadLocalRandom 应运而生。


ThreadLocalRandom

简介

Random 缺点是多个线程会使用同一个原子性种子变量,那么,如果每个线程都维护一个种子变量,则每个线程生成随机数时都根据自己老的种子计算新的种子,并使用新种子更新老的种子,再根据新种子计算随机数就不会存在竞争问题了。(跟 ThreadLocal 的思想很像)。

ThreadLocalRandom 继承自 Random 方法并重写了 nextInt()


使用

public class ThreadLocalRandomTest {
    public static void main(String[] args) {
        ThreadLocalRandom threadLocalRandom = ThreadLocalRandom.current();

        for (int i = 0; i < 5; i++) {
            System.out.println(threadLocalRandom.nextInt(10));
        }
    }
}

使用 ThreadLocalRandom 时,不能直接初始化,只能调用 current() 静态方法。


源码解析

Thread类

Thread 类中有三个变量,由于这些变量只能被当前线程访问,所以没必要加 volatile 关键字了

// 当前线程的种子值
long threadLocalRandomSeed;

// 探测哈希值,如果 threadLocalRandomSeed 已经初始化,则为非 0,否则为 0
int threadLocalRandomProbe;

// 辅助种子,在 LongAdder(很重要) 类中有用
int threadLocalRandomSecondarySeed;

Unsafe机制

获取 threadLocalRandomSeed 等三个变量的偏移量。

private static final sun.misc.Unsafe UNSAFE;
// 记录 Thread 实例中变量 threadLocalRandomSeed 的偏移量
private static final long SEED; 

// 记录 Thread 实例中变量 threadLocalRandomProbe 的偏移量
private static final long PROBE;

// 记录 Thread 实例中变量 threadLocalRandomSecondarySeed 的偏移量
private static final long SECONDARY;
static {
    try {
        // 获取 Unsafe 实例
        UNSAFE = sun.misc.Unsafe.getUnsafe();
        Class<?> tk = Thread.class;
        // 获取 Thread 实例中变量 threadLocalRandomSeed 的偏移量
        SEED = UNSAFE.objectFieldOffset
            (tk.getDeclaredField("threadLocalRandomSeed"));
        // 获取 Thread 实例中变量 threadLocalRandomProbe 的偏移量
        PROBE = UNSAFE.objectFieldOffset
            (tk.getDeclaredField("threadLocalRandomProbe"));
        // 获取 Thread 实例中变量 threadLocalRandomSecondarySeed 的偏移量
        SECONDARY = UNSAFE.objectFieldOffset
            (tk.getDeclaredField("threadLocalRandomSecondarySeed"));
    } catch (Exception e) {
        throw new Error(e);
    }
}

current 方法

所有线程调用 current() 方法返回的都是同一个实例。

    // 所有线程共享一个 ThreadLocalRandom 实例
    static final ThreadLocalRandom instance = new ThreadLocalRandom();
    
    public static ThreadLocalRandom current() {
        // 如果当前线程的 threadLocalRandomProbe 为0,则初始化线程的种子变量
        if (UNSAFE.getInt(Thread.currentThread(), PROBE) == 0)
            localInit();
        return instance;
    }
    
    
    static final void localInit() {
        // 根据某种算法计算 threadLocalRandomProbe 和 threadLocalRandomSeed 的值
        int p = probeGenerator.addAndGet(PROBE_INCREMENT);
        int probe = (p == 0) ? 1 : p; 
        long seed = mix64(seeder.getAndAdd(SEEDER_INCREMENT));
        Thread t = Thread.currentThread();
        // 将 threadLocalRandomSeed 和 threadLocalRandomProbe 设置到当前线程
        UNSAFE.putLong(t, SEED, seed);
        UNSAFE.putInt(t, PROBE, probe);
    }

ThreadLocalRandom 中并没有存放具体的种子,具体的种子存放在具体的调用线程中。当前线程第一次调用 current() 方法时,会初始化当前线程的 threadLocalRandomSeed 变量。


nextInt 方法

public int nextInt(int bound) {
    if (bound <= 0)
        throw new IllegalArgumentException(BadBound);
    
    // 活动随机数
    int r = mix32(nextSeed());
    
   	... 
    // 固定算法
    return r;
}

final long nextSeed() {
    Thread t; long r; 
    UNSAFE.putLong(t = Thread.currentThread(), SEED,
                   r = UNSAFE.getLong(t, SEED) + GAMMA);
    return r;
}

UNSAFE.getLong(t, SEED) 获得当前线程的 threadLocalRandomSeed 的值,再将其加上 GAMMA (一个定值) 作为新种子的值,并更新到当前线程中。


总结:

ThreadLocalRandom 使用 ThreadLocal 的原理,让每个线程都持有一个本地的种子变量,该种子变量只有在使用随机数时才会被初始化。在多线程下计算新种子时是根据自己线程内维护的种子变量进行更新,从而避免了竞争。