抢红包接口出现严重性能问题,竟然是因为使用了这个类!

2021/07/25

生成随机数是一个常见的功能,例如我们给用户生成随机红包。但是你知道java.util.Random类在高并发的情况下具有性能问题和安全问题吗?我们在高并发常见下应该如何生成随机数呢?

要知道为什么java.util.Random类具有性能问题,得深入到其中是实现来探究一下。

首先看一下Random类的doc说明

An instance of this class is used to generate a stream of pseudorandom numbers. The class uses a 48-bit seed, which is modified using a linear congruential formula.

If two instances of Random are created with the same seed, and the same sequence of method calls is made for each, they will generate and return identical sequences of numbers

Random类的实例可以用来生成一个“伪随机”数字流。这个类使用48bit的种子,并且使用线性同余生成器修改种子来实现伪随机。

线性同余的算法可以表示为

linear congruential generator

Random里是怎么实现的呢?Random类使用了xorshift

可以看到Random类中维护了一个seed字段类型为AtomicLong,seed就是随机数的值也叫种子并且通过next方法进行更新。 每次更新的函数为 nextseed = (oldseed * multiplier + addend) & mask,这个函数是确定性的函数,也就是当输入一定的时候,输出也是确定的。所以如果通过Random(long seed)这个构造函数传入固定的seed,则Random生成出来的结果就是固定的,这就是伪随机。

class Random {
    private final AtomicLong seed;
    private static final long multiplier = 0x5DEECE66DL;
    private static final long addend = 0xBL;
    private static final long mask = (1L << 48) - 1;
    public Random() {
        this(seedUniquifier() ^ System.nanoTime());
    }
    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));
        return (int)(nextseed >>> (48 - bits));
    }
}

Java doc里还提到Random类的实例是线程安全的,但是在并发情况下使用可能遇到竞争和性能较差。要在多线程的情况下考虑使用ThreadLocalRandom。

Instances of java.util.Random are threadsafe. However, the concurrent use of the same java.util.Random instance across threads may encounter contention and consequent poor performance. Consider instead using java.util.concurrent.ThreadLocalRandom in multithreaded designs.

这是为什么呢?因为多线程情况下,各个线程调用next方法,next方法的内容是通过AtomicLong.compareAndSet方法cas更新seed值,在多线程情况下,可能出现cas竞争导致频繁重试,导致性能较差。 怎么解决呢?我们能想到避免各个线程使用的共享变量,也就能解决线程间竞争问题,因为每个线程更新自己的数据。一种解决思路是每个线程维护自己的一个Random实例,比如通过ThreadLocal保存每个线程的Random实例。这样能够解决竞争问题,但是带来的问题是导致了比较多的内存占用。

每个线程至少需要创建一个Random类实例,每个类实例除了字段的内存占用还有object header等。

所以最新的ThreadLocalRandom类并没有使用ThreadLocal来实现,

首先,在java.lang.Thread类中,添加了ThreadLocalRandom类使用的threadLocalRandomSeed变量,每个Thread有自己的seed值。并且为了避免出现偶然false sharing问题,字段标记了@Contended注解来避免缓存伪共享问题。并且放到Thread类中,节省了内存空间。

class Thread {
    // The following three initially uninitialized fields are exclusively
    // managed by class java.util.concurrent.ThreadLocalRandom. These
    // fields are used to build the high-performance PRNGs in the
    // concurrent code, and we can not risk accidental false sharing.
    // Hence, the fields are isolated with @Contended.

    /** The current seed for a ThreadLocalRandom */
    @jdk.internal.vm.annotation.Contended("tlr")
    long threadLocalRandomSeed;

    /** Probe hash value; nonzero if threadLocalRandomSeed initialized */
    @jdk.internal.vm.annotation.Contended("tlr")
    int threadLocalRandomProbe
}

在调用ThreadLocalRandom.current()的时候,会触发seed的初始化。current()方法通过Unsafe类修改当前Thread的seed值和prob值(prob值表示是否初始化完成)来进行初始化。 ThreadLocalRandom的nextSeed()方法负责修改seed,修改方式为增加一个GAMMA常量。然后通过mix32等方法适配到long,int等方法的结果上。

class ThreadLocalRandom {
    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();
        U.putLong(t, SEED, seed);
        U.putInt(t, PROBE, probe);
    }

    /**
     * Returns the current thread's {@code ThreadLocalRandom}.
     *
     * @return the current thread's {@code ThreadLocalRandom}
     */
    public static ThreadLocalRandom current() {
        if (U.getInt(Thread.currentThread(), PROBE) == 0)
            localInit();
        return instance;
    }

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

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

    private static int mix32(long z) {
        z = (z ^ (z >>> 33)) * 0xff51afd7ed558ccdL;
        return (int)(((z ^ (z >>> 33)) * 0xc4ceb9fe1a85ec53L) >>> 32);
    }

    private static final long GAMMA = 0x9e3779b97f4a7c15L;
    private static final Unsafe U = Unsafe.getUnsafe();
    private static final long SEED
        = U.objectFieldOffset(Thread.class, "threadLocalRandomSeed");

}

Random和ThreadLocalRandom的问题在于不够密码学安全(比如通过当前的seed,可以推算出下一个seed),如果在安全性要求比较高的场景,需要使用SecureRandom类来生成。

Post Directory