分类

  • 真随机数。通过物理实验得出。比如掷钱币、骰子、转轮、使用电子元件的噪音、核裂变等
  • 伪随机数。通过一定算法和种子得出。软件实现的是伪随机数
  • 强伪随机数。难以预测的随机数
  • 弱伪随机数。易于预测的随机数

随机数的特性

  • 随机性。不存在统计学偏差,完全是杂乱的数列
  • 不可预测性。不能从过去的数列推测出下一个要出现的数
  • 不可重现性。

弱伪随机数只需要满足随机性即可。

强伪随机数需要满足随机性和不可预测性。

真随机数需要同时满足三个特性。

应用场景

  • 验证码生成
  • SessionId/Token 生成
  • 密码

伪随机数的生成

伪随机数的生成实现一般是算法+种子

Pseudo Random Number Generator

伪随机数生成器(PRNG)

  • 线性同余法
  • 单向散列函数法
  • 密码法

常见的是线性同余法,Java 中的 Random 类。

算法可以有很多种,伪随机数的强弱主要取决于种子。

比如 Random 的种子是系统当前的毫秒,所以它的随机数是可以预测的。

比如 SecureRandom 的种子在 Linux 下是选取 /dev/random 的、而它是手机计算机的各种信息得到的。比如键盘的输入、内存的使用状态、各种中断。

Linux操作系统的/dev/random设备接口

Windows操作系统的CryptGenRandom接口

Java 中的随机数

Java随机数比大小编写 java 随机数_SecureRandom

如果不指定种子、则选取当前时间作为种子。

public Random(long seed) {
        if (getClass() == Random.class)
            this.seed = new AtomicLong(initialScramble(seed));
        else {
            // subclass might have overriden setSeed
            this.seed = new AtomicLong();
            setSeed(seed);
        }
    }
    

	public int nextInt(int bound) {
        if (bound <= 0)
            throw new IllegalArgumentException(BadBound);
		
		// 根据旧种子生成新的种子
        int r = next(31);
        int m = bound - 1;
		// 根据种子生成随机数
        if ((bound & m) == 0)  // i.e., bound is a power of 2
            r = (int)((bound * (long)r) >> 31);
        else {
            for (int u = r;
                 u - (r = u % bound) + m < 0;
                 u = next(31))
                ;
        }
        return r;
    }

通过 CAS 生成一个新的种子

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

ThreadLocalRandom

public int nextInt(int bound) {
        if (bound <= 0)
            throw new IllegalArgumentException(BadBound);
		// 根据旧种子生成新的种子
        int r = mix32(nextSeed());
        int m = bound - 1;
		// 根据新种子计算出随机数
        if ((bound & m) == 0) // power of two
            r &= m;
        else { // reject over-represented candidates
            for (int u = r >>> 1;
                 u + m - (r = u % bound) < 0;
                 u = mix32(nextSeed()) >>> 1)
                ;
        }
        return r;
    }

依赖 ThreadLocal

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

我们去 Thread 类中

Java随机数比大小编写 java 随机数_Random_02

Random 和 ThreadLocalRandom 比较

Java随机数比大小编写 java 随机数_java_03

SecureRandom

SecureRandom 默认支持两种加密算法:

SHA1PRNG 算法,提供者 sun.security.provider.SecureRandom。Window 下

NativePRNG 算法,提供者 sun.security.provider.NativePRNG。Linux 下

/dev/random和/dev/urandom

/dev/random和/dev/urandom是unix系统提供的产生随机数的设备,很多应用都需要使用random设备提供的随机数,比如ssh keys, SSL keys, TCP/IP sequence numbers 等等。

# 输出所有的值
cat /dev/random | od -x
# 输出总熵
cat /proc/sys/kernel/random/entropy_avail

Java随机数比大小编写 java 随机数_随机数_04

阻塞问题

soloar代码检测

// 原代码
public void doSomethingCommon() {
  Random rand = new Random();  // Noncompliant; new instance created with each invocation
  int rValue = rand.nextInt();
  //...
// 提出的建议
private Random rand = SecureRandom.getInstanceStrong();  // SecureRandom is preferred to Random

public void doSomethingCommon() {
  int rValue = this.rand.nextInt();
  //...

代码复现

import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;

public class TestRandom {
    public static void main(String[] args) throws NoSuchAlgorithmException {
        System.out.println("start.....");
        long start = System.currentTimeMillis();
        SecureRandom random = SecureRandom.getInstanceStrong();

        for(int i = 0; i < 100; i++) {
            System.out.println("第" + i + "个随机数.");
            random.nextInt(10000);
        }
        System.out.println("finish...time/ms:" + (System.currentTimeMillis() - start));
    }
}

Windows 和 Mac 无法复现。Linux 会出现阻塞

jstack 看线程和栈

"main" #1 prio=5 os_prio=0 tid=0x00007f894c009000 nid=0x1129 runnable [0x00007f8952aa9000]
   java.lang.Thread.State: RUNNABLE
	at java.io.FileInputStream.readBytes(Native Method)
	at java.io.FileInputStream.read(FileInputStream.java:255)
	at sun.security.provider.NativePRNG$RandomIO.readFully(NativePRNG.java:424)
	at sun.security.provider.NativePRNG$RandomIO.ensureBufferValid(NativePRNG.java:525)
	at sun.security.provider.NativePRNG$RandomIO.implNextBytes(NativePRNG.java:544)
	- locked <0x000000076c77cb28> (a java.lang.Object)
	at sun.security.provider.NativePRNG$RandomIO.access$400(NativePRNG.java:331)
	at sun.security.provider.NativePRNG$Blocking.engineNextBytes(NativePRNG.java:268)
	at java.security.SecureRandom.nextBytes(SecureRandom.java:468)
	at java.security.SecureRandom.next(SecureRandom.java:491)
	at java.util.Random.nextInt(Random.java:390)
	at TestRandom.main(TestRandom.java:12)

这里注意到是它会持有一个 Java 锁、其他线程调用该方法时会获取不到锁、然后阻塞

// name of the pure random file (also used for setSeed())
private static final String NAME_RANDOM = "/dev/random";
// name of the pseudo random file
private static final String NAME_URANDOM = "/dev/urandom";

private static RandomIO initIO(final Variant v) {
    return AccessController.doPrivileged(
        new PrivilegedAction<RandomIO>() {
            @Override
            public RandomIO run() {
                File seedFile;
                File nextFile;

                switch(v) {
                //...忽略中间代码
                case BLOCKING: // blocking状态下从/dev/random文件中读取
                    seedFile = new File(NAME_RANDOM);
                    nextFile = new File(NAME_RANDOM);
                    break;

                case NONBLOCKING: // unblocking状态下从/dev/urandom文件中读取数据
                    seedFile = new File(NAME_URANDOM);
                    nextFile = new File(NAME_URANDOM);
                    break;
               	//...忽略中间代码
                try {
                    return new RandomIO(seedFile, nextFile);
                } catch (Exception e) {
                    return null;
                }
            }
    });
}

// constructor, called only once from initIO()
private RandomIO(File seedFile, File nextFile) throws IOException {
    this.seedFile = seedFile;
    seedIn = new FileInputStream(seedFile);
    nextIn = new FileInputStream(nextFile);
    nextBuffer = new byte[BUFFER_SIZE];
}

private void ensureBufferValid() throws IOException {
     long time = System.currentTimeMillis();
     if ((buffered > 0) && (time - lastRead < MAX_BUFFER_TIME)) {
         return;
     }
     lastRead = time;
     readFully(nextIn, nextBuffer);
     buffered = nextBuffer.length;
}

直接尝试读取 /dev/random 文件。

同样会产生阻塞

import java.io.FileInputStream;
import java.io.IOException;

public class TestReadUrandom {

    public static void main(String[] args) throws IOException {
        System.out.println("start.....");
        for(int i = 0; i < 100; i++) {
            System.out.println("第" + i + "次读取随机数");
            FileInputStream inputStream = new FileInputStream("/dev/random");
            byte[] buf = new byte[32];
            inputStream.read(buf, 0, buf.length);
        }
    }

}

c 语言读取。同样阻塞

int main() {
    int randnum = 0;
    int fd = open("/dev/random", O_RDONLY);
    if(fd == -1) {
        printf("open error.\n");
        return 1;
    }
    int i = 0;
    for(i = 0; i < 100; i++) {
        read(fd, (char *)&randnum, sizeof(int));
        printf("random number = %d\n", randnum);
    }
    close(fd);
    return 0;
}
  • 不推荐使用SecureRandom.getInstanceStrong()方式获取SecureRandom(除非对随机要求很高)
  • 推荐使用new SecureRandom()获取SecureRandom, linux下从/dev/urandom读取. 虽然是伪随机, 但大部分场景下都满足。或者使用 SecureRandom.getInstance(“NativePRNGNonBlocking”)

虚拟机环境下和服务器情况类似,输入设备操作很少,相对于 Host 而言,Disk I/O 也相对较少,因此依赖 Guest 自身 PRNG 产生的随机数质量不高,因此虚拟机通常从 Host(宿主机)获取部分随机数据。对于 KVM 虚拟机来说,存在一个半虚拟化设备 virtio-rng 作为硬件随机数产生器。Linux Kernel 从 2.6.26 开始支持 virtio-rng, QEMU 在 1.3 版本加入了对 virtio-rng 的支持。 virtio-rng 设备会读取 Host 的随机数源并且填充到 Guest(客户机)的熵池中。