原创

Java多线程进阶(四六)—— J.U.C之补遗:ThreadLocalRandom

一、ThreadLocalRandom简介

ThreadLocalRandom是在JDK1.7之后引入的一个类,为Random的一个子类,在多线程环境中相较于Random来说它会有更小的开销和争用。

When applicable, use of ThreadLocalRandom rather than shared Random objects in concurrent programs will typically encounter much less overhead and contention.

二、ThreadLocalRandom的原理

Random的介绍

Random的部分定义如下:

public class Random implements java.io.Serializable {
    private final 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)); // 若CAS置新值失败则继续循环
        return (int)(nextseed >>> (48 - bits));
    }  
    ...
}

每一次生成随机数的时候,都会进行CAS操作重置seed,在多个线程使用同一个Random对象时若遇到冲突就会导致线程频繁进行自旋,性能因此受到影响。而为了解决这个问题可以考虑不同的线程有不同的seed,ThreadLocalRandom籍此而生。

ThreadLocalRandom代码剖析

ThreadLocalRandom在解决Random问题的思路上主要是不同的线程使用不同的seed,再根据旧seed生成新seed,线程间seed互不影响,顺着这个思路,JDK团队对在1.7和1.8用了不同的方式来实现同样的目的。以JDK1.8为例,Thread类里面添加了和Random相关的字段:

public class Thread implements Runnable {

    /**
    * ThreadLocalRandom的当前seed 
    */
    @sun.misc.Contended("tlr")   // @sun.misc.Contended为了避免伪共享
    long threadLocalRandomSeed;

    /** 
    * Probe hash 值; 如果初始化了则为非0
    */
    @sun.misc.Contended("tlr")
    int threadLocalRandomProbe;

    /** 
    * 从ThreadLocalRandom公共序列里面分离出来的辅助seed
    */
    @sun.misc.Contended("tlr")
    int threadLocalRandomSecondarySeed;
}

下面的代码让ThreadLocalRandom和上面的Thread属性关联起来:

public class ThreadLocalRandom extends Random {
    private static final sun.misc.Unsafe UNSAFE;
    private static final long SEED;
    private static final long PROBE;
    private static final long SECONDARY;
    static {
        try {
            UNSAFE = sun.misc.Unsafe.getUnsafe();
            Class<?> tk = Thread.class;
            SEED = UNSAFE.objectFieldOffset
                (tk.getDeclaredField("threadLocalRandomSeed"));
            PROBE = UNSAFE.objectFieldOffset
                (tk.getDeclaredField("threadLocalRandomProbe"));
            SECONDARY = UNSAFE.objectFieldOffset
                (tk.getDeclaredField("threadLocalRandomSecondarySeed"));
        } catch (Exception e) {
            throw new Error(e);
        }
    }  
}

继续拜读ThreadLocalRandom的代码定义:

public class ThreadLocalRandom extends Random {
    /**
    * 定义为私有,让外部无法直接调用构造方法初始化对象
    */
    private ThreadLocalRandom() {
        initialized = true; 
    }

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

    /**
    * 定义为static final类型,所有使用ThreadLocalRandom.current()方法的时候得到的是同一个对象
    */
    static final ThreadLocalRandom instance = new ThreadLocalRandom();
    public static ThreadLocalRandom current() {
        if (UNSAFE.getInt(Thread.currentThread(), PROBE) == 0)
            localInit();
        return instance;
    }

    private static final AtomicLong seeder = new AtomicLong(initialSeed());
        /**
        * 设置初始的seed
        */
    private static long initialSeed() {
        String sec = VM.getSavedProperty("java.util.secureRandomSeed"); // 可以通过设置参数来让初始的seed不被预测
        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()));
    }

    ...
}

从代码定义中我们看到,seed已经不再是AtomicLong类型,而直接是在Thread里面定义的long型,再不用担心不同线程在seed上进行CAS操作了。

ThreadLocalRandomcurrent方法在开始时判断当前线程是否已经被初始化过,如果没有,则进行seed值的初始化,同时probe也会被置为非0值,之后同一线程再次调用ThreadLocalRandom.current()时发现probe非0则直接返回instance,而其他线程初次进入的时候,线程的probe仍然是0,则继续初始化。

public int nextInt() {
    return mix32(nextSeed());
}

final long nextSeed() {
    Thread t; long r; // 读取并更新每个线程的seed
    UNSAFE.putLong(t = Thread.currentThread(), SEED,
                   r = UNSAFE.getLong(t, SEED) + GAMMA);
    return r;
}

通过nextInt的代码我们可以看到,每次执行获取随机数时,都是对本线程的数据进行操作,所以线程间的seed互不影响,自然没有Random中存在的问题。

三、总结

ThreadLocalRandom通过巧妙的设计,将最初Random共享而影响性能的seed挪到了Thread本身,让各个Thread有独立的seed来进行随机数的处理,避免了多线程对同一Random实例的争用。

Random的设计中,其seed可以是被指定的,而在此类中计算随机数的算法是固定的,在具有同等seed的情况下生成的随机序列是确定且一致的,因此ThreadLocalRandom不支持setSeed方法,且不允许通过其他方式设置seed,尽量避免随机数序列一样的情况,不过因为ThreadLocalRandom仍然是伪随机数生成器,即使指定了初始seed是java.util.secureRandomSeed,若要使用更严格的随机数,请使用SecureRandom,但是要面对性能损失。

正文到此结束
该篇文章的评论功能已被站长关闭
本文目录