公司最近有一个项目需要使用到随机数,用户通过抽奖的方式获得奖品。

public static int random(int min,int max){
        Random r=new Random();
        return  r.nextInt(max-min)+min;
    }

逻辑很简单,就是产生[min,max)范围的随机数。这种实现问题是否会有问题?

业界常用的随机算法,希望从实现原理分析一下一个简单随机算法使用的正确姿势。

Random

首先我们要知道random是一个伪随机算法,并不是一个真正的随机算法。

* If two instances of {@code 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的实例通过一个相同的seed构建,则会产生相同的数字序列。

public Random() {
        this(seedUniquifier() ^ System.nanoTime());
    }

    private static long seedUniquifier() {
        // L'Ecuyer, "Tables of Linear Congruential Generators of
        // Different Sizes and Good Lattice Structure", 1999
        for (;;) {
            long current = seedUniquifier.get();
            long next = current * 181783497276652981L;
            if (seedUniquifier.compareAndSet(current, next))
                return next;
        }
    }

如果不指定初始化的seed,则会以当前的系统时间(精确到纳秒)来初始化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));
        return (int)(nextseed >>> (48 - bits));
    }

生成随机数的过程本质上就是更新seed的过程。通过代码我们可以知道Random本身是线程安全的(通过CAS保证)

所以我们再回到一开始讲到的业务代码,每次去new Random()会有问题吗?

我们知道随机数算法是通过当前的seed来计算下一次的seed,如果每次去new一个实例,同一个时间点的中奖结果必然是一样的。

另外Random保证的是单实例下的多次调用的概率分布的平衡,通过每次new一个实例是无法保证整个概率分布的合理性。

我们尝试把代码修改为如下:

private static final class RandomNumberGeneratorHolder {
        static final Random randomNumberGenerator = new Random();
    }

public static int random(int min,int max){
        return  RandomNumberGeneratorHolder.randomNumberGenerator.nextInt(max-min)+min;
    }

保证在多线程环境下使用一个实例进行随机。

然而在高并发环境下会由于需要对seed进行加锁而导致性能下降,因此考虑使用ThreadLocalRandom来解决性能的问题。

ThreadLocalRandom

由于Random类在多线程环境会共享同一个seed的变量,锁会导致随机数生成的性能下降。ThreadLocalRandom就是为了解决这个问题存在的。

解决的思路是为每一个线程单独分配一个seed。

我们先看下Thread的源码,保留了两个属性。

/** The current seed for a ThreadLocalRandom */
    @sun.misc.Contended("tlr")
    long threadLocalRandomSeed;

    /** Probe hash value; nonzero if threadLocalRandomSeed initialized */
    @sun.misc.Contended("tlr")
    int threadLocalRandomProbe;

接下来看ThreadLocalRandom的current()方法。

/**
     * Returns the current thread's {@code ThreadLocalRandom}.
     *
     * @return the current thread's {@code 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);
    }
    

主要为了初始化Thread的threadLocalRandomSeed和threadLocalRandomProbe两个属性。在初始化的过程中并不是简单地通过获取线程的属性进行更新,而是通过unsafe(JNI)的方式进行。SEED和PROBE分别是这两个属性的偏移量。

另外初始化的种子为seeder增加一个常量来初始化。seeder为静态变量,但是只有在初始化的时候才会使用到,所以对性能影响不大。

// We must define this, but never use it.
    protected int next(int bits) {
        return (int)(mix64(nextSeed()) >>> (64 - bits));
    }
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;
    }

生成的随机数的方法跟Random类似,通过获取当前的seed按照一定的算法规则进行更新,并返回给调用方。

SecureRandom

在看ThreadLocalRandom源码的时候,我们发现在初始化seed的方法里面有使用到SecureRandom的方法。我们回想一下,如果初始化的种子被破解,那么其实整个随机序列都会被得知,而初始化的种子如果是完全依赖服务器时间的话,用户可以在某个时间点上操作来控制整个随机算法。

SecureRandom来保证初始化的种子更难以被猜测。

private static long initialSeed() {
        String pp = java.security.AccessController.doPrivileged(
                new sun.security.action.GetPropertyAction(
                        "java.util.secureRandomSeed"));
        if (pp != null && pp.equalsIgnoreCase("true")) {
            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()));
    }

参考文献:

并发包中ThreadLocalRandom类原理浅尝

sun.misc.Unsafe的理解