解決SecureRandom.getInstanceStrong()引發的線程阻塞問題
1. 背景介紹
sonar掃描到使用Random隨機函數不安全, 推薦使用SecureRandom替換之, 當使用SecureRandom.getInstanceStrong()獲取SecureRandom並調用next方式時, 在生產環境(linux)產生較長時間的阻塞, 但開發環境(windows7)並未重現
2. 現象展示
使用測試代碼:
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 + "個隨機數."); random.nextInt(10000); } System.out.println("finish...time/ms:" + (System.currentTimeMillis() - start)); } }
2.1 windows7下運行結果
第94個隨機數.
第95個隨機數.
第96個隨機數.
第97個隨機數.
第98個隨機數.
第99個隨機數.
finish…time/ms:100
windows下未出現明顯阻塞現象, 耗時100ms
2.2 centos7下運行結果
第52個隨機數.
第53個隨機數.
第54個隨機數.
第55個隨機數.
第56個隨機數.
第57個隨機數.
第58個隨機數.
第59個隨機數.
第60個隨機數.
第61個隨機數.
第62個隨機數.
第63個隨機數.
第64個隨機數.
…
linux下運行阻塞在第65次獲取隨機數.(如果實驗結果未阻塞, 可以嘗試增加獲取隨機數的次數)
3. 現象分析
3.1 linux阻塞分析
通過
jstack -l <你的java進程>
得到如下堆棧信息
“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)這個讀取文件的IO處.
對NativePRNG的部分關鍵源碼進行分析:
// 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狀態下從/dev/random文件中讀取 seedFile = new File(NAME_RANDOM); nextFile = new File(NAME_RANDOM); break; case NONBLOCKING: // unblocking狀態下從/dev/urandom文件中讀取數據 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; }
從源代碼分析, 發現導致阻塞的原因是因為從/dev/random中讀取隨機數導致, 可以通過如下代碼驗證:
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 + "次讀取隨機數"); FileInputStream inputStream = new FileInputStream("/dev/random"); byte[] buf = new byte[32]; inputStream.read(buf, 0, buf.length); } } }
上述代碼在linux環境下同樣會產生阻塞.
通過hotspot源碼分析, java通過c調用操作系統的讀取文件api, 通過一個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; }
這個例子再次論證瞭讀取/dev/random會導致阻塞
3.2 windows下運行結果分析
- NativePRNG.java這個文件在linux和windows下的環境中實現不同
- windows的調用堆棧過程
- windows在通過SecureRandom.getInstanceStrong()獲取隨機數的過程, 並沒有使用到NativePRNG, 而是最終調用sun.security.mscapi.PRNG#generateSeed的native方法, 所以windows並沒有明顯的阻塞現象(但明顯比 new SecureRandom()生成的對象產生隨機數要慢許多).
- sun.security.mscapi.PRNG#generateSeed的native方法實現, 閱讀hotspot中security.cpp代碼
#include <windows.h> JNIEXPORT jbyteArray JNICALL Java_sun_security_mscapi_PRNG_generateSeed (JNIEnv *env, jclass clazz, jint length, jbyteArray seed) { //省略不關鍵代碼... else if (length > 0) { pbData = new BYTE[length]; if (::CryptGenRandom( // 此處通過調用windows提供的apiCryptGenRandom獲取隨機數 hCryptProv, length, pbData) == FALSE) { ThrowException(env, PROVIDER_EXCEPTION, GetLastError()); __leave; } result = env->NewByteArray(length); env->SetByteArrayRegion(result, 0, length, (jbyte*) pbData); } //省略不關鍵代碼... }
沒有詳細研究CryptGenRandom的具體實現
4. 結論
4.1 推薦使用方式
- 不推薦使用SecureRandom.getInstanceStrong()方式獲取SecureRandom(除非對隨機要求很高)
- 推薦使用new SecureRandom()獲取SecureRandom, linux下從/dev/urandom讀取. 雖然是偽隨機, 但大部分場景下都滿足.
4.2 關於/dev/random的擴展
- 由於/dev/random中的數據來自系統的擾動, 比如鍵盤輸入, 鼠標點擊, 等等, 當系統擾動很小時, 產生的隨機數不夠, 導致讀取/dev/random的進程會阻塞等待. 可以做個小實驗, 當阻塞時, 多點擊鼠標, 鍵盤輸入數據等操作, 會加速結束阻塞
- 可以從通過這個命令cat /proc/sys/kernel/random/entropy_avail獲取當前系統的熵, 值越大, /dev/random中隨機數產生效率越高
- 熵補償:可通過安裝linux下的工具haveged, 進行系統熵補償, 安裝後, 啟動haveged, 發現系統熵值從幾十增加到一千多, 此時在運行前面阻塞的程序(運行結果如下), 發現不再阻塞, 獲取100個隨機數隻要29毫秒, 效率大大提升.
第91個隨機數.
第92個隨機數.
第93個隨機數.
第94個隨機數.
第95個隨機數.
第96個隨機數.
第97個隨機數.
第98個隨機數.
第99個隨機數.
finish…time/ms:29
以上為個人經驗,希望能給大傢一個參考,也希望大傢多多支持WalkonNet。
推薦閱讀:
- 你知道jdk竟有4個random嗎
- 解決JDBC Connection Reset的問題分析
- Java創建隨機數的四種方式總結
- Docker環境下Spring Boot應用內存飆升分析與解決場景分析
- java基礎知識之FileInputStream流的使用