Java提供两种类型的随机数发生器
1.伪随机数发生器
伪随机数发生器采用特定的算法,将随机数种子seed转换成一系列的伪随机数。伪随机数依赖于seed的值,给定相同的seed值总是生成相同的随机数。伪随机数的生成过程只依赖CPU,不依赖任何外部设备,生成速度快,不会阻塞。
Java提供的伪随机数发生器有java.util.Random类和java.util.concurrent.ThreadLocalRandom类。
Random类采用AtomicLong实现,保证多线程的线程安全性,但正如该类注释上说明的,多线程并发获取随机数时性能较差。
多线程环境中可以使用ThreadLocalRandom作为随机数发生器,ThreadLocalRandom采用了线程局部变量来改善性能,这样就可以使用long而不是AtomicLong,此外,ThreadLocalRandom还进行了字节填充,以避免伪共享。
2.强随机数发生器
强随机数发生器依赖于操作系统底层提供的随机事件。强随机数生成器的初始化速度和生成速度都较慢,而且由于需要一定的熵累积才能生成足够强度的随机数,所以可能会造成阻塞。熵累积通常来源于多个随机事件源,如敲击键盘的时间间隔,移动鼠标的距离与间隔,特定中断的时间间隔等。所以,只有在需要生成加密性强的随机数据的时候才用它。
Java提供的强随机数发生器是java.security.SecureRandom类,该类也是一个线程安全类,使用synchronize方法保证线程安全,但jdk并没有做出承诺在将来改变SecureRandom的线程安全性。因此,同Random一样,在高并发的多线程环境中可能会有性能问题。
在linux的实现中,可以使用/dev/random和/dev/urandom作为随机事件源。由于/dev/random是堵塞的,在读取随机数的时候,当熵池值为空的时候会堵塞影响性能,尤其是系统大并发的生成随机数的时候,如果在随机数要求不高的情况下,可以去读取/dev/urandom来避免阻塞,方法是通过设置参数
-Djava.security.egd=file:/dev/urandom
但这样由于jdk的一个bug,实际上需要这样指定这个值
-Djava.security.egd=file:/dev/./urandom
原因是,在 sun.security.provider.SunEntries类,seedSource先读取系统参数java.security.egd,如果值为空的时候,读取java.security配置文件中的参数securerandom.source, 在通常情况下,就是读取参数securerandom.source,默认值是/dev/urandom。
sun.security.provider.SeedGenerator final static String URL_DEV_RANDOM = SunEntries.URL_DEV_RANDOM; final static String URL_DEV_URANDOM = SunEntries.URL_DEV_URANDOM; if (egdSource.equals(URL_DEV_RANDOM) || egdSource.equals(URL_DEV_URANDOM)) { try { instance = new NativeSeedGenerator(); if (debug != null) { debug.println("Using operating system seed generator"); } } catch (IOException e) { if (debug != null) { debug.println("Failed to use operating system seed " + "generator: " + e.toString()); } } } else if (egdSource.length() != 0) { try { instance = new URLSeedGenerator(egdSource); if (debug != null) { debug.println("Using URL seed generator reading from " + egdSource); } } catch (IOException e) { if (debug != null) debug.println("Failed to create seed generator with " + egdSource + ": " + e.toString()); } }
在代码中可以看到当配置值是file:/dev/random或者file:/dev/urandom的时候,启用NativeSeedGenerator, 而在linux下的NativeSeedGenerator类的实现是这样的
class NativeSeedGenerator extends SeedGenerator.URLSeedGenerator { NativeSeedGenerator() throws IOException { super(); } }
而URLSeedGenerator的默认构造方法是
URLSeedGenerator() throws IOException { this(SeedGenerator.URL_DEV_RANDOM); }
也就是说哪怕设置了-Djava.security.egd=file:/dev/urandom,最后的结果一样是读取file:/dev/random,解决办法就是使用linux的多种路径表示法,即使用file:/dev/./urandom来绕过这个问题。