解決SecureRandom.getInstanceStrong()引發(fā)的線程阻塞問(wèn)題
1. 背景介紹
sonar掃描到使用Random隨機(jī)函數(shù)不安全, 推薦使用SecureRandom替換之, 當(dāng)使用SecureRandom.getInstanceStrong()獲取SecureRandom并調(diào)用next方式時(shí), 在生產(chǎn)環(huán)境(linux)產(chǎn)生較長(zhǎng)時(shí)間的阻塞, 但開發(fā)環(huán)境(windows7)并未重現(xiàn)
2. 現(xiàn)象展示
使用測(cè)試代碼:
package com.youai.test;
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 + "個(gè)隨機(jī)數(shù).");
random.nextInt(10000);
}
System.out.println("finish...time/ms:" + (System.currentTimeMillis() - start));
}
}
2.1 windows7下運(yùn)行結(jié)果
第94個(gè)隨機(jī)數(shù).
第95個(gè)隨機(jī)數(shù).
第96個(gè)隨機(jī)數(shù).
第97個(gè)隨機(jī)數(shù).
第98個(gè)隨機(jī)數(shù).
第99個(gè)隨機(jī)數(shù).
finish...time/ms:100
windows下未出現(xiàn)明顯阻塞現(xiàn)象, 耗時(shí)100ms
2.2 centos7下運(yùn)行結(jié)果
第52個(gè)隨機(jī)數(shù).
第53個(gè)隨機(jī)數(shù).
第54個(gè)隨機(jī)數(shù).
第55個(gè)隨機(jī)數(shù).
第56個(gè)隨機(jī)數(shù).
第57個(gè)隨機(jī)數(shù).
第58個(gè)隨機(jī)數(shù).
第59個(gè)隨機(jī)數(shù).
第60個(gè)隨機(jī)數(shù).
第61個(gè)隨機(jī)數(shù).
第62個(gè)隨機(jī)數(shù).
第63個(gè)隨機(jī)數(shù).
第64個(gè)隨機(jī)數(shù).
...
linux下運(yùn)行阻塞在第65次獲取隨機(jī)數(shù).(如果實(shí)驗(yàn)結(jié)果未阻塞, 可以嘗試增加獲取隨機(jī)數(shù)的次數(shù))
3. 現(xiàn)象分析
3.1 linux阻塞分析
通過(guò)
jstack -l <你的java進(jìn)程>
得到如下堆棧信息
"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)
可以看到main線程阻塞在了java.io.FileInputStream.readBytes(Native Method)這個(gè)讀取文件的IO處.
對(duì)NativePRNG的部分關(guān)鍵源碼進(jìn)行分析:
// 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狀態(tài)下從/dev/random文件中讀取
seedFile = new File(NAME_RANDOM);
nextFile = new File(NAME_RANDOM);
break;
case NONBLOCKING: // unblocking狀態(tài)下從/dev/urandom文件中讀取數(shù)據(jù)
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;
}
從源代碼分析, 發(fā)現(xiàn)導(dǎo)致阻塞的原因是因?yàn)閺?dev/random中讀取隨機(jī)數(shù)導(dǎo)致, 可以通過(guò)如下代碼驗(yàn)證:
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 + "次讀取隨機(jī)數(shù)");
FileInputStream inputStream = new FileInputStream("/dev/random");
byte[] buf = new byte[32];
inputStream.read(buf, 0, buf.length);
}
}
}
上述代碼在linux環(huán)境下同樣會(huì)產(chǎn)生阻塞.
通過(guò)hotspot源碼分析, java通過(guò)c調(diào)用操作系統(tǒng)的讀取文件api, 通過(guò)一個(gè)c代碼的案例論證:
#include <stdio.h>
#include <fcntl.h>
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;
}
這個(gè)例子再次論證了讀取/dev/random會(huì)導(dǎo)致阻塞
3.2 windows下運(yùn)行結(jié)果分析
- NativePRNG.java這個(gè)文件在linux和windows下的環(huán)境中實(shí)現(xiàn)不同
- windows的調(diào)用堆棧過(guò)程

- windows在通過(guò)SecureRandom.getInstanceStrong()獲取隨機(jī)數(shù)的過(guò)程, 并沒(méi)有使用到NativePRNG, 而是最終調(diào)用sun.security.mscapi.PRNG#generateSeed的native方法, 所以windows并沒(méi)有明顯的阻塞現(xiàn)象(但明顯比 new SecureRandom()生成的對(duì)象產(chǎn)生隨機(jī)數(shù)要慢許多).
- sun.security.mscapi.PRNG#generateSeed的native方法實(shí)現(xiàn), 閱讀hotspot中security.cpp代碼
#include <windows.h>
JNIEXPORT jbyteArray JNICALL Java_sun_security_mscapi_PRNG_generateSeed
(JNIEnv *env, jclass clazz, jint length, jbyteArray seed)
{
//省略不關(guān)鍵代碼...
else if (length > 0) {
pbData = new BYTE[length];
if (::CryptGenRandom( // 此處通過(guò)調(diào)用windows提供的apiCryptGenRandom獲取隨機(jī)數(shù)
hCryptProv,
length,
pbData) == FALSE) {
ThrowException(env, PROVIDER_EXCEPTION, GetLastError());
__leave;
}
result = env->NewByteArray(length);
env->SetByteArrayRegion(result, 0, length, (jbyte*) pbData);
}
//省略不關(guān)鍵代碼...
}
沒(méi)有詳細(xì)研究CryptGenRandom的具體實(shí)現(xiàn)
4. 結(jié)論
4.1 推薦使用方式
- 不推薦使用SecureRandom.getInstanceStrong()方式獲取SecureRandom(除非對(duì)隨機(jī)要求很高)
- 推薦使用new SecureRandom()獲取SecureRandom, linux下從/dev/urandom讀取. 雖然是偽隨機(jī), 但大部分場(chǎng)景下都滿足.
4.2 關(guān)于/dev/random的擴(kuò)展
- 由于/dev/random中的數(shù)據(jù)來(lái)自系統(tǒng)的擾動(dòng), 比如鍵盤輸入, 鼠標(biāo)點(diǎn)擊, 等等, 當(dāng)系統(tǒng)擾動(dòng)很小時(shí), 產(chǎn)生的隨機(jī)數(shù)不夠, 導(dǎo)致讀取/dev/random的進(jìn)程會(huì)阻塞等待. 可以做個(gè)小實(shí)驗(yàn), 當(dāng)阻塞時(shí), 多點(diǎn)擊鼠標(biāo), 鍵盤輸入數(shù)據(jù)等操作, 會(huì)加速結(jié)束阻塞
- 可以從通過(guò)這個(gè)命令cat /proc/sys/kernel/random/entropy_avail獲取當(dāng)前系統(tǒng)的熵, 值越大, /dev/random中隨機(jī)數(shù)產(chǎn)生效率越高
- 熵補(bǔ)償:可通過(guò)安裝linux下的工具h(yuǎn)aveged, 進(jìn)行系統(tǒng)熵補(bǔ)償, 安裝后, 啟動(dòng)haveged, 發(fā)現(xiàn)系統(tǒng)熵值從幾十增加到一千多, 此時(shí)在運(yùn)行前面阻塞的程序(運(yùn)行結(jié)果如下), 發(fā)現(xiàn)不再阻塞, 獲取100個(gè)隨機(jī)數(shù)只要29毫秒, 效率大大提升.
第91個(gè)隨機(jī)數(shù).
第92個(gè)隨機(jī)數(shù).
第93個(gè)隨機(jī)數(shù).
第94個(gè)隨機(jī)數(shù).
第95個(gè)隨機(jī)數(shù).
第96個(gè)隨機(jī)數(shù).
第97個(gè)隨機(jī)數(shù).
第98個(gè)隨機(jī)數(shù).
第99個(gè)隨機(jī)數(shù).
finish...time/ms:29
以上為個(gè)人經(jīng)驗(yàn),希望能給大家一個(gè)參考,也希望大家多多支持腳本之家。
相關(guān)文章
spring AOP的After增強(qiáng)實(shí)現(xiàn)方法實(shí)例分析
這篇文章主要介紹了spring AOP的After增強(qiáng)實(shí)現(xiàn)方法,結(jié)合實(shí)例形式分析了spring面向切面AOP的After增強(qiáng)實(shí)現(xiàn)步驟與相關(guān)操作技巧,需要的朋友可以參考下2020-01-01
Spring?Data?JPA系列JpaSpecificationExecutor用法詳解
這篇文章主要為大家介紹了Spring?Data?JPA系列JpaSpecificationExecutor用法詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-09-09
spring?security需求分析與基礎(chǔ)環(huán)境準(zhǔn)備教程
這篇文章主要為大家介紹了spring?security需求分析與基礎(chǔ)環(huán)境準(zhǔn)備教程,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步2022-03-03
Spring?MVC啟動(dòng)之HandlerMapping作用及實(shí)現(xiàn)詳解
這篇文章主要為大家介紹了Spring?MVC啟動(dòng)之HandlerMapping作用及實(shí)現(xiàn)詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-03-03
SpringMVC結(jié)合Jcrop實(shí)現(xiàn)圖片裁剪
這篇文章主要介紹了SpringMVC結(jié)合Jcrop實(shí)現(xiàn)圖片裁剪的相關(guān)資料,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2016-12-12
SpringBoot如何統(tǒng)一清理數(shù)據(jù)
這篇文章主要介紹了SpringBoot如何統(tǒng)一清理數(shù)據(jù)問(wèn)題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2024-01-01
Java使用excel工具類導(dǎo)出對(duì)象功能示例
這篇文章主要介紹了Java使用excel工具類導(dǎo)出對(duì)象功能,結(jié)合實(shí)例形式分析了java創(chuàng)建及導(dǎo)出Excel數(shù)據(jù)的具體步驟與相關(guān)操作技巧,需要的朋友可以參考下2017-10-10

