关于随机数的基本概念
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 产生随机数采坑记录 - 腾讯云开发者社区-腾讯云