再谈随机数引起的阻塞问题

Java的随机数实现有很多坑,记录一下这次使用jdk1.8里新增的加强版随机数实现SecureRandom.getInstanceStrong() 遇到的问题。

之前在维护ali-tomcat的时候曾发现过jvm随机数算法选用不当导致tomcat的SessionID生成非常慢的情况,可以参考JVM上的随机数与熵池策略 和 Docker中apache-tomcat启动慢的问题 这两篇文章。不过当时没有太追究,以为使用了-Djava.security.egd=file:/dev/./urandom就可以避免了,在这次项目里再次遇到随机数导致所有线程阻塞之后发现这块还挺多规则。

本次项目中使用的是jdk1.8,启动参数里设置了

-Djava.security.egd=file:/dev/./urandom

使用的随机数方式是Java8新增的:

SecureRandom.getInstanceStrong();

碰到故障时,线程阻塞在

"DubboServerHandler-xxx:20880-thread-1789" #28440 daemon prio=5 os_prio=0 tid=0x0000000008ffd000 nid=0x5712 runnable [0x000000004cbd7000]
java.lang.Thread.State: RUNNABLE
    at java.io.FileInputStream.readBytes(Native Method)
    at java.io.FileInputStream.read(FileInputStream.java:246)
    at sun.security.provider.NativePRNG$RandomIO.readFully(NativePRNG.java:410)
    at sun.security.provider.NativePRNG$RandomIO.implGenerateSeed(NativePRNG.java:427)
    - locked <0x00000000c03a3c90> (a java.lang.Object)
    at sun.security.provider.NativePRNG$RandomIO.access$500(NativePRNG.java:329)
    at sun.security.provider.NativePRNG$Blocking.engineGenerateSeed(NativePRNG.java:272)
    at java.security.SecureRandom.generateSeed(SecureRandom.java:522)

因为这个地方有加锁,locked <0x00000000c03a3c90>,所以其它线程调用到这里时会等待这个lock:

"DubboServerHandler-xxx:20880-thread-1790" #28441 daemon prio=5 os_prio=0 tid=0x0000000008fff000 nid=0x5713 waiting for monitor entry [0x000000004ccd8000]
java.lang.Thread.State: BLOCKED (on object monitor)
    at sun.security.provider.NativePRNG$RandomIO.implGenerateSeed(NativePRNG.java:424)
    - waiting to lock <0x00000000c03a3c90> (a java.lang.Object)
    at sun.security.provider.NativePRNG$RandomIO.access$500(NativePRNG.java:329)
    at sun.security.provider.NativePRNG$Blocking.engineGenerateSeed(NativePRNG.java:272)
    at java.security.SecureRandom.generateSeed(SecureRandom.java:522)

去查 NativePRNG$Blocking代码,看到它的文档描述:

A NativePRNG-like class that uses /dev/random for both seed and random material. Note that it does not respect the egd properties, since we have no way of knowing what those qualities are.

奇怪怎么-Djava.security.egd=file:/dev/./urandom参数没起作用,仍使用/dev/random作为随机数的熵池,时间久或调用频繁的话熵池很容易不够用而导致阻塞;于是看了一下SecureRandom.getInstanceStrong()的文档:

Returns a SecureRandom object that was selected by using the algorithms/providers specified in the securerandom.strongAlgorithms Security property.

原来有自己的算法,在 jre/lib/security/java.security 文件里,默认定义为:

securerandom.strongAlgorithms=NativePRNGBlocking:SUN

如果修改算法值为NativePRNGNonBlocking:SUN的话,会采用NativePRNG$NonBlocking里的逻辑,用/dev/urandom作为熵池,不会遇到阻塞问题。但这个文件是jdk系统文件,修改它或重新指定一个路径都有些麻烦,最好能通过系统环境变量来设置,可这个变量不像securerandom.source属性可以通过系统环境变量-Djava.security.egd=xxx来配置,找半天就是没有对应的系统环境变量。只好修改代码,不采用SecureRandom.getInstanceStrong这个新方法,改成了SecureRandom.getInstance("NativePRNGNonBlocking")

对于SecureRandom的两种算法实现:SHA1PRNG 和 NativePRNG 跟 securerandom.source 变量的关系,找到一篇解释的很清楚的文章:Using the SecureRandom Class

On Linux:

1) when this value is “file:/dev/urandom” then the NativePRNG algorithm is registered by the Sun crypto provider as the default implementation; the NativePRNG algorithm then reads from /dev/urandom for nextBytes but /dev/random for generateSeed

2) when this value is “file:/dev/random” then the NativePRNG algorithm is not registered by the Sun crypto provider, but the SHA1PRNG system uses a NativeSeedGenerator which reads from /dev/random.

3) when this value is anything else then the SHA1PRNG is used with a URLSeedGenerator that reads from that source.

4) when the value is undefined, then SHA1PRNG is used with ThreadedSeedGenerator

5) when the code explicitly asks for “SHA1PRNG” and the value is either “file:/dev/urandom” or “file:/dev/random” then (2) also occurs

6) when the code explicitly asks for “SHA1PRNG” and the value is some other “file:” url, then (3) occurs

7) when the code explicitly asks for “SHA1PRNG” and the value is undefined then (4) occurs

至于SHA1PRNG算法里,为何用urandom时,不能直接设置为file:/dev/urandom而要用变通的方式设置为file:///dev/urandom或者 file:/dev/./urandom,参考这里

In SHA1PRNG, there is a SeedGenerator which does various things depending on the configuration.

  1. If java.security.egd or securerandom.source point to “file:/dev/random” or “file:/dev/urandom”, we will use NativeSeedGenerator, which calls super() which calls SeedGenerator.URLSeedGenerator(/dev/random). (A nested class within SeedGenerator.) The only things that changed in this bug was that urandom will also trigger use of this code path.

  2. If those properties point to another URL that exists, we’ll initialize SeedGenerator.URLSeedGenerator(url). This is why “file:///dev/urandom”, “file:/./dev/random”, etc. will work.

转自:http://hongjiang.info/java8-nativeprng-blocking/

### 三级标题:问题分析 在 STM32 中使用 `HAL_GetTick()` 生成 32 个非零随机数时,主要存在以下几个潜在问题: 1. **种子熵值不足** `HAL_GetTick()` 返回的是系统运行时间,以毫秒为单位递增。若程序在短时间内多次调用 `srand(HAL_GetTick())` 设置种子,可能导致生成的随机数序列相似度较高,降低随机性。例如,在 `rand_num()` 函数中,若连续调用该函数,可能生成重复的随机数 [^2]。 类似地,在生成 32 个非零随机数时,若每次调用 `generate_random_number()` 都仅依赖 `HAL_GetTick()` 获取种子,可能导致生成的随机数之间缺乏足够差异,影响随机性。 2. **线性同余法的局限性** 使用线性同余法(LCG)生成伪随机数虽然实现简单,但其周期性和分布特性较差。例如,使用 `seed = (seed * 1103515245) + 12345` 的公式生成随机数,虽然可以生成 32 位整数,但其分布可能存在偏倚,导致某些数值更容易重复出现。此外,若初始种子相同,生成的随机数序列也将完全相同,进一步影响随机性。 3. **非零检查逻辑可能导致阻塞** 在生成 32 个非零随机数时,若采用循环不断生成并检查是否为零的策略,可能导致程序在极端情况下长时间阻塞。例如,如果随机数生成算法偶尔生成零值,程序需要不断重新生成,直到获得非零值,这在实时性要求较高的嵌入式系统中可能造成任务延迟。 4. **SysTick 中断依赖性** `HAL_GetTick()` 依赖于 SysTick 中断的正常运行。若 SysTick 初始化失败或中断未正确触发,`uwTick` 变量将不会递增,导致 `HAL_GetTick()` 返回值不变,从而影响随机数生成的随机性。这在某些仿真环境中可能表现异常,例如 Keil 仿真器中默认未模拟 `uwTick` 的递增行为 [^3]。 ### 三级标题:优化建议 为提升随机数生成的质量和稳定性,可采取以下优化措施: - **引入额外熵源** 结合外部事件(如按键按下时间、ADC 采样值、外部中断时间戳等)作为额外熵源,与 `HAL_GetTick()` 结合生成种子,提高种子的不可预测性。例如,可使用 `srand(HAL_GetTick() ^ ADC_Value)` 提升种子多样性。 - **使用高质量随机数生成算法** 替换线性同余法为更高质量的伪随机数生成算法,如 Mersenne Twister 或 AES-CTR 模式生成伪随机数流,提升随机数分布的均匀性和周期长度。 - **避免阻塞式非零检查** 在生成随机数时,可采用位掩码操作确保最低位为 1,例如 `(num | 1)`,从而保证生成的数始终为非零值,避免循环等待带来的延迟问题。 - **使用硬件 RNG 模块** 若 STM32 芯片支持硬件随机数发生器(RNG),如 STM32L475 系列,应优先使用硬件 RNG 模块生成高质量随机数 [^1]。该模块基于物理噪声源,具备更高的随机性和安全性。 ### 三级标题:示例代码改进 以下为优化后的随机数生成函数,结合额外熵源和位掩码避免阻塞: ```c #include "stm32f4xx_hal.h" #include "adc.h" // 假设 ADC 已初始化 // 使用 HAL_GetTick 和 ADC 采样值作为种子生成随机数 uint32_t generate_random_number(void) { static uint32_t seed = 0; seed = HAL_GetTick() ^ ((uint32_t)HAL_ADC_GetValue(&hadc1) << 16); // 结合 ADC 采样值 seed = (seed * 1103515245) + 12345; // 线性同余法 return seed; } // 生成 32 个非零随机数,避免阻塞 void generate_32_nonzero_random_numbers(uint32_t *buffer) { for (uint32_t i = 0; i < 32; i++) { buffer[i] = generate_random_number() | 1; // 确保最低位为 1,避免为 0 } } ``` 此实现提升了种子熵值,并通过位操作确保生成的随机数非零,避免循环等待。 ###
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值