java伪随机数生成器

阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6

关于随机数的基本概念

1、对随机数性质分类

  • 随机性符合该性质的叫弱伪随机数。这种随机数仅可以用于一般应用无法用在密码学例如java中的java.util.Random类
  • 不可预测性符合该性质的叫强伪随机数。在密码学中随机数除了随机性还需要不可预测性也就是无法通过历史数据推测下一个随机数
  • 不可重复性符合该性质的叫真随机数。仅靠软件无法生成不可重复的随机数这是因为计算机软件仅具备有限内部状态只要内部状态相同必然生成同一个数这也叫做周期。要满足不可重复性只能通过物理现象获取比如温度、声音等这些需要硬件支持比如英特尔CPU中提供了一个随机数生成器。

上述性能越往下越严格且具有包含性质比如不可重复性一定具有随机性和不可预测性不可预测性一定具有随机性。

说明反复投骰子生成的数列具有不可重复性。

2、总结

随机数可以通过硬件、软件来生成。通过硬件生成的随机数例如利用传感器收集的热量、声音等具有无法预测不重复的自然现象称作真随机数也叫随机数生成器Random Number Generator RNG。

通过软件生成的随机数叫做为随机数Pseudo Random Number Generator PRNG。因为软件状态有限一定具有周期性。为随机数分为弱为随机和强为随机。

接下来我们介绍java中的伪随机数使用。

Random介绍

在JDK7之前java.util.Random是使用比较广泛的随机数生成工具类另外java.lang.Math中的随机数生成也是使用的java.util.Random的实例。Random类使用的是线性同余算法生成随机数该算法是一种弱伪随机数算法不具备不可预测性所以不能用于密码学。

1、Random类是线程安全的

Random随机数生成是和种子seed有关而为了保证线程安全性Random通过CAS机制来保证线程安全性。从next()方法中我们可以看到seed是通过不断的CAS来进行修改值的。如果在高并发的场景下那么可能会导致CAS失败从而导致不断的自旋这样可能会导致CPU过高。

此外在创建Random的时候也是用CAS

这里也可以看到默认Random的种子适合当前时间有关的当然也可以在构造函数中指定seed。

 2、使用

Random random = new Random();
random.nextInt(); //返回[0, max)的随机正整数
random.nextInt(10); //返回[0,10)的随机正整数
        
double rd1 = Math.random(); //返回[0.0,1.0)的随机小数

ThreadLocalRandom介绍

ThreadLocalRandom继承Random也是弱伪随机数。它主要解决了Random在高并发环境下随机数生成性能问题。

1、ThreadLocalRandom为什么线程安全

和ThreadLocal原理类似它将随机种子保存在当前Thread对象的threadLocalRandomSeed变量中这样每个线程都有自己的随机种子实现了线程级别的隔离所以ThreadLocalRandom也并不需要像Random通过自旋锁和cas来保证随机种子的线程安全性。在高并发的场景下效率也会相对较高。

2、源码实现

ThreadLocalRandom大量使用了unsafe方法来直接通过地址操作变量。关于unfafe用法见3.0 Java魔法类Unsafe应用解析.note

1种子定义在Thread类在ThreadLocalRandom定义的SEED表示Thread类的threadLocalRandomSeed属性的内存偏移量

ThreadLocalRandom.java
private static final sun.misc.Unsafe UNSAFE;
private static final long SEED;
private static final long PROBE;
private static final long SECONDARY;
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);
    }
}

Thread.java
@sun.misc.Contended("tlr")
//当前Thread的随机种子 默认值是0
long threadLocalRandomSeed;
@sun.misc.Contended("tlr")
//用来标志当前Thread的threadLocalRandomSeed是否进行了初始化 0代表没有非0代表已经初始化 默认值是0
int threadLocalRandomProbe;

说明其中 @sun.misc.Contende 是为了避免伪共享。

2当线程调用ThreadLocalRandom的current方法ThreadLocalRandom负责初始化调用线程的threadLocalRandomSeed变量也就是初始化种子

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

说明通过unsafe的putLong方法根据对象属性偏移地址直接操作内存。

3当调用ThreadLocalRandom的nextInt方法时候实际上是获取当前线程的threadLocalRandomSeed变量作为当前种子来计算新的值然后更新新的种子到当前线程的threadLocalRandomSeed变量。线性同余算法根据种子计算随机数然后将随机数更新成新的种子供下次计算

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

并发高的情况下试试用ThreadLocalRandom来生成随机数 - 腾讯云开发者社区-腾讯云

3、说明

在ssh架构中无论是Random、ThreadLocalRandom都不能放在全局变量中定义都应该放到方法中去定义、初始化。否则就有可能不同线程使用了相同的种子这样创建出来的随机数序列可能是一样的。即

java.Security.SecureRandom介绍

也是继承java.util.Random该类提供了强伪随机数算法可以在密码学中使用。

1、Linux 中随机数是如何产生的呢 PRNG(Pseudo-Random Number Generator)

Linux 内核采用熵来描述数据的随机性熵entropy是描述系统混乱无序程度的物理量熵越大则说明该系统的有序性越差。内核维护了一个熵池用来收集来自设备驱动程序和其它来源的环境噪音。内核中PRNG 为一个字符设备 random它提供了 2 个字符设备供用户态进程使用——/dev/random 和/dev/urandom

  • /dev/random 适用于对随机数质量要求比较高的请求在熵池中数据不足时 读取 dev/random 设备时会返回小于熵池噪声总数的随机字节。/dev/random 可生成高随机性的公钥或一次性密码本。若熵池空了对/dev/random 的读操作将会被阻塞直到收集到了足够的环境噪声为止。这样的设计使得/dev/random 是真正的随机数发生器提供了最大可能的随机数据熵。
  • /dev/urandom非阻塞的随机数发生器它会重复使用熵池中的数据以产生伪随机数据。这表示对/dev/urandom 的读取操作不会产生阻塞但其输出的熵可能小于/dev/random 的。它可以作为生成较低强度密码的伪随机数生成器对大多数应用来说随机性是可以接受的。

2、获取SecureRandom实例方法

有两种方式获取SecureRandom实例

  • SecureRandom random = new SecureRandom(); //也可以通过参数指定种子但不建议这么做。
  • Random random = SecureRandom.getInstanceStrong();

这两种方式的区别在linux下第一种采用的是/dev/urandom作为伪随机数发生器后者采用的是/dev/random作为伪随机数发生器。所以后者可以能会因为系统的熵池数据不足导致其调用nextInt等方法获取随机数时阻塞。

1实验

import java.security.SecureRandom;
import java.util.Random;
import java.security.NoSuchAlgorithmException;
public class RandomTest {
    public static void main(String... str) throws NoSuchAlgorithmException {
        //SecureRandom random = new SecureRandom();
        Random random = SecureRandom.getInstanceStrong();
        int out = 0;
        for (int i = 0; i < 1<<20 ; i++) {
            out ^= random.nextInt();
        }
        System.out.println(out);
    }
}

该程序分别用两种不同方式创建SecureRandom实例然后产生1048576次随机数。在linux上经过测试发现使用第6行方式创建SecureRandom实例程序在2s内执行完如果使用第7行方式程序会阻塞。这也验证了上面的结论。

2通过strace跟踪系统调用

strace命令可以跟踪linux的系统调用。所以我们对上面的程序进行如下运行

A、使用第6行方式创建SecureRandom实例然后javac编译通过如下方式运行

javac RandomTest.java
strace -ff -o ./c.strace  java RandomTest
1839562559
#会在当前目录产生大量的strace文件通过执行如下命令搜索"dev/random"所在的文件
grep "dev" -r -n ./
./c.strace.22057:6:openat(AT_FDCWD, "/sys/devices/system/cpu", O_RDONLY|O_NONBLOCK|O_CLOEXEC|O_DIRECTORY) = 3
./c.strace.22057:966:access("/dev/random", R_OK)             = 0
./c.strace.22057:967:access("/dev/random", R_OK)             = 0
./c.strace.22057:968:access("/dev/urandom", R_OK)            = 0
./c.strace.22057:969:open("/dev/random", O_RDONLY)           = 5
./c.strace.22057:970:fstat(5, {st_mode=S_IFCHR|0666, st_rdev=makedev(1, 8), ...}) = 0
./c.strace.22057:971:open("/dev/urandom", O_RDONLY)          = 6
./c.strace.22057:972:fstat(6, {st_mode=S_IFCHR|0666, st_rdev=makedev(1, 9), ...}) = 0
./c.strace.22057:973:access("/dev/random", R_OK)             = 0
./c.strace.22057:974:access("/dev/random", R_OK)             = 0
./c.strace.22057:975:open("/dev/random", O_RDONLY)           = 7
./c.strace.22057:976:fstat(7, {st_mode=S_IFCHR|0666, st_rdev=makedev(1, 8), ...}) = 0
./c.strace.22057:977:open("/dev/random", O_RDONLY)           = 8
./c.strace.22057:978:fstat(8, {st_mode=S_IFCHR|0666, st_rdev=makedev(1, 8), ...}) = 0
./c.strace.22057:979:access("/dev/urandom", R_OK)            = 0
./c.strace.22057:980:access("/dev/urandom", R_OK)            = 0
./c.strace.22057:981:open("/dev/urandom", O_RDONLY)          = 9
./c.strace.22057:982:fstat(9, {st_mode=S_IFCHR|0666, st_rdev=makedev(1, 9), ...}) = 0
./c.strace.22057:985:open("/dev/urandom", O_RDONLY)          = 10
./c.strace.22057:986:fstat(10, {st_mode=S_IFCHR|0666, st_rdev=makedev(1, 9), ...}) = 0
./c.strace.21889:96:fstat(1, {st_mode=S_IFCHR|0620, st_rdev=makedev(136, 0), ...}) = 0
./c.strace.22065:10:open("/sys/devices/system/cpu/online", O_RDONLY|O_CLOEXEC) = 3

打开文件

可以看到虽然SecureRandom.java底层会打开/dev/random和/dev/urandom这两个文件但最终读取的却是6号也就是/dev/urandom文件。

B、使用第7行方式创建SecureRandom实例然后javac编译同样方式运行

strace -ff -o c.strace  java RandomTest
#会一直在这里阻塞住没有执行结果。。。

同样方式找到文件打开

可以看到SecureRandom.java底层会打开/dev/random和/dev/urandom这两个文件但最终读取的却是8号也就是/dev/random文件同时read系统调用一直阻塞住了。

linux - Java SecureRandom doesn't block? How? - Information Security Stack Exchange

使用 SecureRandom 产生随机数采坑记录 - 腾讯云开发者社区-腾讯云

 

 

 

 

 

 

阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6
标签: Java