前言
生成伪随机数是用Java编程时的常见需求,本文简单讨论一下最常用的Random和ThreadLocalRandom这两个随机数类,顺便介绍线性同余法。
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());
}
private static long seedUniquifier() {
for (;;) {
long current = seedUniquifier.get();
long next = current * 181783497276652981L;
if (seedUniquifier.compareAndSet(current, next))
return next;
}
}
private static final AtomicLong seedUniquifier
= new AtomicLong(8682522807148012L);
public Random(long seed) {
if (getClass() == Random.class)
this.seed = new AtomicLong(initialScramble(seed));
else {
this.seed = new AtomicLong();
setSeed(seed);
}
}
private static long initialScramble(long seed) {
return (seed ^ multiplier) & mask;
}
如果我们构造Random实例时,调用的是有参构造方法(即传入了自定义的随机数种子seed),那么通过initialScramble()方法产生的实际使用的种子为:
(seed XOR 0x5DEECE66DL) AND ((1L << 48) - 1)
可见,种子的长度是48比特。
如果调用的是无参构造方法,那么会将seedUniquifier()方法返回的值与当前JVM运行的纳秒时间戳做异或运算,并作为seed值,再经过上式的运算得出实际使用的种子。而seedUniquifier()是个自旋的方法,它以8682522807148012L
作为初始值,不断与181783497276652981L
相乘,直到某次相乘前后的结果在long范围内相同时才返回。这两个大数的来历请参见StackOverflow上的这个问答。
下面看看生成伪随机数的核心方法next(),其参数bits是随机数的字长,比如nextInt()方法调用next()方法时,bits就是32。
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更新种子的值为:
(seed * 0x5DEECE66DL + 0xBL) & ((1L << 48) - 1)
然后将新的种子值右移(48 - bits)位,得到随机数的真值。上式就是所谓线性同余法的实现,下面简单分析一下。
线性同余法
线性同余法(linear congruential generator, LCG)是非常古老的伪随机数生成方法,在1958年提出,但时至如今,它仍然是使用最广泛的方法之一。它的思想很简单,用一个递推式即可概括:
X[n+1] = (a·X[n] + c) mod m
其中:
- m > 0,称为模(modulus);
- 0 < a < m,称为乘数(multiplier);
- 0 <= c < m,称为增量(increment);
- X[0],即递推序列的初始值,称为种子(seed)。X就是生成的随机数序列。
LCG有如下的重要性质(Hull-Dobell定理):
当且仅当
(1) c与m互素
(2) a - 1可以被所有m的质因数整除
(3) 如果m能被4整除,那么a - 1也能被4整除
以上三个条件同时满足时,X是一个周期为m的序列。
既然LCG产生的是周期性的伪随机数序列,为了使它难以预测,m的值一般都会选得非常大。Random类中a、c、m这三个参数的取值分别为:
a = 0x5DEECE66DL = 25214903917 // multiplier
c = 0xBL = 11 //addend
m = 1L << 48 = 281474976710656 // mask
可见,这三个值是满足Hull-Dobell定理的,故Random的周期为248,足够大了。
为了方便理解,下图示出不同参数取值下的LCG周期。可见,当不满足上述定理时(第一行和第二行),X的周期是一定比m短的。
特别地,c=0的特殊情况叫做Lehmer生成器(Lehmer RNG),又叫乘同余法(multiplicative congruential generator, MCG)。它的出现比LCG更早(1951年),并且参数的取值有更多的限制。本文不再展开讲细节,看官可以参见LCG与MCG的英文维基相关页面。
上面所叙述的过程产生的是整数,如果要产生(0, 1)区间内的小数怎么办呢?很简单,结果再除以m就行了,因为X中的原始值肯定比m小。
ThreadLocalRandom
阿里Java开发手册中如是说:
【推荐】避免Random实例被多线程使用,虽然共享该实例是线程安全的,但会因竞争同一seed导致性能下降。
说明:Random实例包括java.util.Random的实例或者Math.random()的方式。
正例:在JDK7之后,可以直接使用API ThreadLocalRandom,而在JDK7之前,需要编码保证每个线程持有一个实例。
从代码可知,由于Random类中的种子是AtomicLong类型,在多线程进入next()方法时,只有一个线程能CAS成功更新种子,其他线程都在不断自旋,性能降低。为了解决这个问题,产生了ThreadLocalRandom。
ThreadLocalRandom是Random的子类,但在JDK7与JDK8中的实现方法颇有不同。下面是JDK7版本的主要源码。
private static final long multiplier = 0x5DEECE66DL;
private static final long addend = 0xBL;
private static final long mask = (1L << 48) - 1;
private long rnd;
boolean initialized;
// 填充字节,避免不同线程的ThreadLocalRandom竞争CPU缓存行造成虚共享
private long pad0, pad1, pad2, pad3, pad4, pad5, pad6, pad7;
ThreadLocalRandom() {
super();
initialized = true;
}
private static final ThreadLocal localRandom =
new ThreadLocal() {
protected ThreadLocalRandom initialValue() {
return new ThreadLocalRandom();
}
};
public static ThreadLocalRandom current() {
return localRandom.get();
}
public void setSeed(long seed) {
if (initialized)
throw new UnsupportedOperationException();
rnd = (seed ^ multiplier) & mask;
}
protected int next(int bits) {
rnd = (rnd * multiplier + addend) & mask;
return (int) (rnd >>> (48-bits));
}
可见,ThreadLocalRandom产生随机数的思路与Random完全相同,不过是利用ThreadLocal为每个线程维护了不同的ThreadLocalRandom实例。在ThreadLocalRandom初始化时,会调用Random的无参构造方法,并通过重写过的setSeed()方法将计算出的种子存入rnd变量,故每个线程都拥有了不同的种子,通过current()方法即可取得自己的那一份,不会再产生冲突。
下面是JDK8版本的主要源码。
private static final AtomicInteger probeGenerator =
new AtomicInteger();
private static final AtomicLong seeder = new AtomicLong(initialSeed());
boolean initialized;
private ThreadLocalRandom() {
initialized = true;
}
static final ThreadLocalRandom instance = new ThreadLocalRandom();
static final void localInit() {
int p = probeGenerator.addAndGet(PROBE_INCREMENT);
int probe = (p == 0) ? 1 : p;
long seed = mix64(seeder.getAndAdd(SEEDER_INCREMENT));
Thread t = Thread.currentThread();
UNSAFE.putLong(t, SEED, seed);
UNSAFE.putInt(t, PROBE, probe);
}
public static ThreadLocalRandom current() {
if (UNSAFE.getInt(Thread.currentThread(), PROBE) == 0)
localInit();
return instance;
}
public void setSeed(long seed) {
if (initialized)
throw new UnsupportedOperationException();
}
final long nextSeed() {
Thread t; long r;
UNSAFE.putLong(t = Thread.currentThread(), SEED,
r = UNSAFE.getLong(t, SEED) + GAMMA);
return r;
}
protected int next(int bits) {
return (int)(mix64(nextSeed()) >>> (64 - bits));
}
这些源码中并没有出现ThreadLocal,那么每个线程的种子去哪里了?答案是加进了Thread类里,作为独立的字段存在。
// sun.misc.Contended注解用于填充缓存行
@sun.misc.Contended("tlr")
long threadLocalRandomSeed;
@sun.misc.Contended("tlr")
int threadLocalRandomProbe;
threadLocalRandomSeed就是随机数种子,threadLocalRandomProbe是指示种子是否被初始化的探针变量。可见,JDK8虽然没有显式使用ThreadLocal,但是基本的思想是相通的——即每个线程都持有独享的种子值。ThreadLocalRandom代码中的SEED和PROBE两个量,实际上是通过Unsafe API取得的字段偏移地址。
private static final sun.misc.Unsafe UNSAFE;
private static final long SEED;
private static final long PROBE;
static {
try {
UNSAFE = sun.misc.Unsafe.getUnsafe();
Class> tk = Thread.class;
SEED = UNSAFE.objectFieldOffset
(tk.getDeclaredField("threadLocalRandomSeed"));
PROBE = UNSAFE.objectFieldOffset
(tk.getDeclaredField("threadLocalRandomProbe"));
} catch (Exception e) {
throw new Error(e);
}
}
JDK8版ThreadLocalRandom只有一个全局的实例,通过current()方法取得。当某个Thread实例的探针threadLocalRandomProbe为0时,表示是首次执行current()方法,进而会调用localInit()方法初始化种子和探针的值,并将它们写入对应的Thread实例中。通过nextSeed()方法就可以分别更新对应线程的种子了。
可见,与JDK7版本相比,JDK8版本的ThreadLocalRandom不再依赖于父类Random的构造方法产生种子,也就彻底消除了CAS自旋带来的开销。关于这个改进的细节,还可以参考StackOverflow上的这个问题。
最后一丢丢
上面讲的LCG、MCG是伪随机数发生器,也就是说如果知道了a、c、m参数的值,随机数就被破解了,无法做到安全。要保证安全,可以采用SecureRandom类。它利用了Linux的特殊设备/dev/random和/dev/urandom,基于系统的熵(进程数、内存用量、设备输入等无法预测的指标)产生随机数,这样也就没办法预测随机序列的值了。
晚安晚安。