Java-随机数

文章详细介绍了随机数的类型,包括真随机数和伪随机数(强伪随机数与弱伪随机数),并讲解了随机数的特性。在Java中,Random类用于生成伪随机数,而SecureRandom提供更安全的随机数生成,特别是在Linux下使用/dev/urandom来避免阻塞问题。ThreadLocalRandom则为线程安全的随机数生成器。文章还提到了Linux下的/dev/random和/dev/urandom设备以及它们在随机数生成中的作用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

分类

  • 真随机数。通过物理实验得出。比如掷钱币、骰子、转轮、使用电子元件的噪音、核裂变等

  • 伪随机数。通过一定算法和种子得出。软件实现的是伪随机数

    • 强伪随机数。难以预测的随机数
    • 弱伪随机数。易于预测的随机数

随机数的特性

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

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

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

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

应用场景

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

伪随机数的生成

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

Pseudo Random Number Generator

伪随机数生成器(PRNG)

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

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

种子的选取

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

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

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

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

Windows操作系统的CryptGenRandom接口

Java 中的随机数

Random

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

    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 类中

Random 和 ThreadLocalRandom 比较

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

阻塞问题

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(客户机)的熵池中。

https://juejin.cn/post/6844903572883128327

https://juejin.cn/s/安全随机数的要求是不可预测%2C绝对安全

https://juejin.cn/post/6844904096785235982

https://blog.csdn.net/fengye_csdn/article/details/120843570

https://blog.csdn.net/weixin_45244678/article/details/106137948

https://hongjiang.info/java8-nativeprng-blocking/

众所周知,随机数是任何一种编程语言最基本的特征之一。而生成随机数的基本方式也是相同的:产生一个0到1之间的随机数。看似简单,但有时我们也会忽略了一些有趣的功能。 我们从书本上学到什么? 最明显的,也是直观的方式,在Java中生成随机数只要简单的调用: 1.java.lang.Math.random() 在所有其他语言中,生成随机数就像是使用Math工具类,如abs, pow, floor, sqrt和其他数学函数。大多数人通过书籍、教程和课程来了解这个类。一个简单的例子:从0.0到1.0之间可以生成一个双精度浮点数。那么通过上面的信息,开发人员要产生0.0和10.0之间的双精度浮点数会这样来写: 1.Math.random() * 10 而产生0和10之间的整数,则会写成: 1.Math.round(Math.random() * 10) 进阶 通过阅读Math.random()的源码,或者干脆利用IDE的自动完成功能,开发人员可以很容易发现,java.lang.Math.random()使用一个内部的随机生成对象 - 一个很强大的对象可以灵活的随机产生:布尔值、所有数字类型,甚至是高斯分布。例如: 1.new java.util.Random().nextInt(10) 它有一个缺点,就是它是一个对象。它的方法必须是通过一个实例来调用,这意味着必须先调用它的构造函数。如果在内存充足的情况下,像上面的表达式是可以接受的;但内存不足时,就会带来问题。 一个简单的解决方案,可以避免每次需要生成一个随机数时创建一个新实例,那就是使用一个静态类。猜你可能想到了java.lang.Math,很好,我们就是改良java.lang.Math的初始化。虽然这个工程量低,但你也要做一些简单的单元测试来确保其不会出错。 假设程序需要生成一个随机数来存储,问题就又来了。比如有时需要操作或保护种子(seed),一个内部数用来存储状态和计算下一个随机数。在这些特殊情况下,共用随机生成对象是不合适的。 并发 在Java EE多线程应用程序的环境中,随机生成实例对象仍然可以被存储在类或其他实现类,作为一个静态属性。幸运的是,java.util.Random是线程安全的,所以不存在多个线程调用会破坏种子(seed)的风险。 另一个值得考虑的是多线程java.lang.ThreadLocal的实例。偷懒的做法是通过Java本身API实现单一实例,当然你也可以确保每一个线程都有自己的一个实例对象。 虽然Java没有提供一个很好的方法来管理java.util.Random的单一实例。但是,期待已久的Java 7提供了一种新的方式来产生随机数: 1.java.util.concurrent.ThreadLocalRandom.current().nextInt(10) 这个新的API综合了其他两种方法的优点:单一实例/静态访问,就像Math.random()一样灵活。ThreadLocalRandom也比其他任何处理高并发的方法要更快。 经验 Chris Marasti-Georg 指出: 1.Math.round(Math.random() * 10) 使分布不平衡,例如:0.0 - 0.499999将四舍五入为0,而0.5至1.499999将四舍五入为1。那么如何使用旧式语法来实现正确的均衡分布,如下: 1.Math.floor(Math.random() * 11) 幸运的是,如果我们使用java.util.Randomjava.util.concurrent.ThreadLocalRandom就不用担心上述问题了。 Java实战项目里面介绍了一些不正确使用java.util.Random API的危害。这个教训告诉我们不要使用: 1.Math.abs(rnd.nextInt())%n 而使用: 1.rnd.nextInt(n)
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值