解决SecureRandom.getInstanceStrong()引发的线程阻塞问题

目录
  • 1. 背景介绍
  • 2. 现象展示
    • 2.1 windows7下运行结果
    • 2.2 centos7下运行结果
  • 3. 现象分析
    • 3.1 linux阻塞分析
    • 3.2 windows下运行结果分析
  • 4. 结论
    • 4.1 推荐使用方式
    • 4.2 关于/dev/random的扩展

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

以上为个人经验,希望能给大家一个参考,也希望大家多多支持我们。

(0)

相关推荐

  • java调用process线程阻塞问题的解决

    java调用process线程阻塞问题 项目需求中涉及java调用.bat文件进行图像处理,先直接上简略版程序 public void draw(){ //调用bat脚本进行图像处理 Process process = null; InputStream in = null; try { process = Runtime.getRuntime().exec("startup.bat"); //输出测试 // in = process.getInputStream(); // Stri

  • 聊聊Java中是什么方法导致的线程阻塞

    一.为什么引入线程阻塞机制? 为了解决对共享存储区的访问冲突,Java 引入了同步机制,现在让我们来考察多个线程对共享资源的访问,显然同步机制已经不够了,因为在任意时刻所要求的资源不一定已经准备好了被访问,反过来,同一时刻准备好了的资源也可能不止一个.为了解决这种情况下的访问控制问题,Java 引入了对阻塞机制的支持. 阻塞指的是暂停一个线程的执行以等待某个条件发生(如某资源就绪),学过操作系统的同学对它一定已经很熟悉了.Java 提供了大量方法来支持阻塞,下面让我们逐一分析. 二.Java中实

  • Java多线程阻塞与唤醒代码示例

    java线程的阻塞及唤醒 1. sleep() 方法: sleep(-毫秒),指定以毫秒为单位的时间,使线程在该时间内进入线程阻塞状态,期间得不到cpu的时间片,等到时间过去了,线程重新进入可执行状态.(暂停线程,不会释放锁) //测试sleep()方法 class Thread7 implements Runnable{ @Override public void run() { for(int i=0;i<50;i++){ System.out.println(Thread.currentT

  • Java线程阻塞方法sleep()与wait()的全面讲解

    一.前期基础知识储备 sleep()和wait()方法都是Java中造成线程阻塞的方法.感兴趣的读者可以参见笔者之前的文章<Java中什么方法导致线程阻塞>,里面详细讲述了为什么Java要造成线程阻塞和Java中造成线程阻塞的几种方法. (1)线程的生命周期 这是笔者在谷歌图片中找到的一张简单描述线程生命周期的图片,可以看到,一个线程正常的生命周期中会经历"创建""就绪""运行""阻塞""运行"

  • 解决SecureRandom.getInstanceStrong()引发的线程阻塞问题

    目录 1. 背景介绍 2. 现象展示 2.1 windows7下运行结果 2.2 centos7下运行结果 3. 现象分析 3.1 linux阻塞分析 3.2 windows下运行结果分析 4. 结论 4.1 推荐使用方式 4.2 关于/dev/random的扩展 1. 背景介绍 sonar扫描到使用Random随机函数不安全, 推荐使用SecureRandom替换之, 当使用SecureRandom.getInstanceStrong()获取SecureRandom并调用next方式时, 在生

  • 详解jQuery同步Ajax带来的UI线程阻塞问题及解决办法

    俗话说不作死就不会死,今天作死了一回,写了一个比较二逼的函数,遇到了同步Ajax引起的UI线程阻塞问题,在此记录一下. 事情起因是这样的,因为页面上有多个相似的异步请求动作,本着提高代码可重用性的原则,我封装了一个名为getData的函数,它接收不同参数,只负责获取数据,然后把数据return.基本的逻辑剥离出来是这样的: function getData1(){ var result; $.ajax({ url : 'p.php', async : false, success: functi

  • Jquery ajax 同步阻塞引起的UI线程阻塞问题

    最近做一个项目,遇到了一个问题同步ajax引起的ui线程阻塞问题,下面把我的问题解决过程分享给大家. 事情起因是这样的,因为页面上有多个相似的异步请求动作,本着提高代码可重用性的原则,我封装了一个名为getData的函数,它接收不同参数,只负责获取数据,然后把数据return.基本的逻辑剥离出来是这样的: function getData1(){ var result; $.ajax({ url : "p.php", async : false, success: function(d

  • 线程阻塞唤醒工具 LockSupport使用详解

    目录 LockSupport 简介 回顾 synchronized 和 Lock LockSupport 和 synchronized 和 Lock 的阻塞方式对比 LockSupport 的使用 LockSupport 注意事项 许可证提前发放 许可证不会累计 LockSupport 底层实现 结语 LockSupport 简介 LockSupport 是 Java 并发编程中一个非常重要的组件,我们熟知的并发组件 Lock.线程池.CountDownLatch 等都是基于 AQS 实现的,而

  • 详解Java多线程编程中LockSupport类的线程阻塞用法

    LockSupport是用来创建锁和其他同步类的基本线程阻塞原语. LockSupport中的park() 和 unpark() 的作用分别是阻塞线程和解除阻塞线程,而且park()和unpark()不会遇到"Thread.suspend 和 Thread.resume所可能引发的死锁"问题. 因为park() 和 unpark()有许可的存在:调用 park() 的线程和另一个试图将其 unpark() 的线程之间的竞争将保持活性. 基本用法 LockSupport 很类似于二元信号

  • 解决python多行注释引发缩进错误的问题

    如下所示: m_start =date +' 09:00' m_end =date +' 13:00' rsv_1 ={ 'act':'set_resv', 'dev_id':dev_id, 'start':m_start, 'end':m_end, } ''' rsv_2 ={ '_':'', 'act':'set_resv', 'dev_id':dev_id, 'start':'2018-05-21 13:00', 'end_time':'', 'kind_id':'', 'lab_id':

  • 浅谈C#跨线程调用窗体控件(比如TextBox)引发的线程安全问题

    如何:对 Windows 窗体控件进行线程安全调用 访问 Windows 窗体控件本质上不是线程安全的. 如果有两个或多个线程操作某一控件的状态,则可能会迫使该控件进入一种不一致的状态. 还可能会出现其他与线程相关的 Bug,例如争用情况和死锁. 确保以线程安全方式访问控件非常重要. 在未使用 Invoke 方法的情况下,从不是创建某个控件的线程的其他线程调用该控件是不安全的. 以下非线程安全的调用的示例. // This event handler creates a thread that

  • 关于HttpClient 引发的线程太多导致FullGc的问题

    CloseableHttpClient httpClient = HttpClients.custom() .setConnectionManager(connectionManager) .setMaxConnTotal(400) .setMaxConnPerRoute(150) .evictExpiredConnections() .build(); evictExpiredConnections 这个配置作用: 设置一个定时线程,定时清理闲置连接,可以将这个定时时间设置为 keep ali

随机推荐