Java 随机数比较和分析(一)

2014-11-24 08:59:20 · 作者: · 浏览: 2

概况:
本文概述2种jdk的随机数实现方式,旨在了解其运行机理。并得出运行效率比较。但这2种随机数生成还是会存在一定安全风险(伪随机数有可能会被猜出随机序列),最后还给出另一种相对更安全的随机数产生方式。附录还给出jdk的nextInt(n)函数的代码分析。

一. 2种产生方式:
一般通过jdk获取0~N(N为自然数)的随机数可以通过下面2种方式获取

1. Math.random() ——返回[0,1)的随机小数,通过(int) (n * Math.random())即可获取[0,n)的随机数

2. java.util.Random的nextInt(n)方法 ——返回[0,n)的随机小数

在jdk1.6实现中,Math.random()实现如下:

[java]
public static double random() {
if (randomNumberGenerator == null) initRNG(); // randomNumberGenerator是Math中持有的Random类单例
return randomNumberGenerator.nextDouble();
}
randomNumberGenerator 原来Math.random()还是基于Random类实现了随机数生成。
下面再看下Random类的nextDouble()和nextInt(n)分别怎么实现:

[java]
public double nextDouble() {
return (((long)(next(26)) << 27) + next(27))
/ (double)(1L << 53);
}
[java] view plaincopy
public int nextInt(int n) {
if (n <= 0)
throw new IllegalArgumentException("n must be positive");

if ((n & -n) == n) // i.e., n is a power of 2
return (int)((n * (long)next(31)) >> 31);

int bits, val;
do {
bits = next(31);
val = bits % n;
} while (bits - val + (n-1) < 0); // 这里为什么要这么判断,请见附录a
return val;
}
从2段程序中可以看到,最终实现随机数的生成还是依赖于next(n)。
(注:next(n)的功能是产生n位bits,每个bit位随机为0或1,当n<32时,next(n)产生的随机数范围为0~2^31-1,当n=32时,产生随机数范围为-2的31次方~2^31-1。源码中的next函数最终返回return (int)(nextseed >>> (48 - bits)) 是因为随机种子是48位,所以最终返回的是随机种子的高n位bit)
而nextDouble()中铁定调用了2次next(n),而nextInt(n)则不确定,但平均调用next(n)的次数在2次以下(见附录b证明)。所以nextInt(n)比nextDouble()获取随机数的效率要高。

另外,通过(int) (n * Math.random())获取的整数随机数其实是通过double随机数近似而来,准确性相对nextInt(n)来说应该会低些。

二. 下面是运行速度比较:
[java]
public class RandomTest{
static Random random = new Random();

static int r1(int n) {
return (int)(Math.random() * n);
}
static int r2(int n) {
return random.nextInt(n);
}

public static void main(String[] args) {
long t1 = System.nanoTime();
for (int i=0;i<10000;i++) {
r1(1024);
}
long t2 = System.nanoTime();
System.out.println(t2 - t1);
t1 = System.nanoTime();
for (int i=0;i<10000;i++) {
r2(1024);
}
t2 = System.nanoTime();
System.out.println(t2 - t1);
}
}
结果:
3422502
1335365

结论:无论从准确性和效率,nextInt(n)都比Math.random()要好。

三. 安全性
Random类的next(n)方法依靠确定的seed种子来计算nextseed的值(seed位48位的bit),尽管使用了各种运算,但结果仍然是线性可预测的。

[java]
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确定,那么nextseed也就确定,那整个序列都可以被重建出来。故如果对于随机的情景,如果攻击者获取了最初的种子seed,那么他将可以轻易模拟出随机数,并得到下一个seed。或者他通过穷举seed来获取计算的随机数来匹配程序的随机数。
所以对安全性有要求的随机数应用情景,可以用java.security.SecureRandom。代替伪随机的Random类。该类继承自Random类,并覆盖了next(n)函数,所以可以利用其提供的强随机的种子算法(SHA1PRNG)来生成随机数。

效率上肯定有损失,大概相差1个数量级。

[java]
static int r3(int n) {
final int offset = 123456; // offset为固定值,避免被猜到种子来源(和密码学中的加salt有点类似)
long seed = System.currentTimeMillis() + offset;
SecureRandom secureRandom1;
try {
secureRandom1 = SecureRandom.getInstance("SHA1PRNG");

secureRandom1.setSeed(seed);
return secureRandom1.nextInt();
} catch (NoSuchAlgorithmException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
return 0;
}

四. 附录
a. nextInt(n)函数解析

该函数进行的工作是把31位的原始随机范围next(31)的结果映射到