Java线程池阻塞问题场景分析

背景:

        每10s会向线程池(此业务专用)写入一个任务,任务内容是:使用CMD命令行从显卡驱动来获取GPU信息

问题:

        有时候会碰到显卡异常的情况,这种就获取不到,线程池目前用了10个线程,有时候卡住获取比较慢,就会导致线程一直不释放,导致线程池提交任务异常

问:

        阻塞的线程 如何释放,此场景如何优化

当时有问小伙伴此方式有没有提供超时参数相关,小伙伴当时没回复,今日回复有测过 Process.waitFor(),

但是没效果(o(╥﹏╥)o当时没看清是通过CMD的,误以为是显卡提供的接口,导致后面的排查方向错了,但是也误打误撞走进另外一条路,有了新的收获)

先放一个思路

        采用Process.waitFor【等价FuturnTask.get】指定参数来设置最大超时等待时间(实际使用需考虑死锁问题---主进程在等待,子进程写满缓冲区,如果非要使用waitFor,那就在waitFor之前单独启2个额外的线程,分别用于处理标准输入流 和 标准错误流)

给一段伪代码:

    String cmd = "路径\\test.bat";
    //echo sleep start
    //ping 127.0.0.1 -n 5 > nul
    //echo sleep end
    try {
    Process process = Runtime.getRuntime().exec(cmd);
    process.waitFor(6, TimeUnit.SECONDS);//有需要可处理死锁问题,没有则不需要
    
    process.exitValue();
    // ||  两行二选一执行--先埋坑放这里
    try{
        process.exitValue();
    } catch (Exception e){
        e.printStackTrace();
    }
    
    InputStream is = process.getInputStream();
    InputStreamReader isr = new InputStreamReader(is);
    BufferedReader br = new BufferedReader(isr);
    String content = br.readLine();
    while (content != null) {
        System.out.println(content);
        content = br.readLine();
    }
    } catch (IOException | InterruptedException e) {
    e.printStackTrace();
    }

这里的解题思路其实是指定任务执行的超时时间,其他场景:rpc中的连接超时、读超时时间、尝试获取锁的最大等待时间等等的配置和这都是一个意思(如果一个任务有较多的子流程,可以挨个控制每个子流程的最大执行时间,从而让整个任务的最大执行时间可控)

下面继续聊当时考虑的另外一个方向以后的踩坑之路(因认为没有最大超时时间的配置 故当时的思路就到另外的方向了)

        小伙伴一开始的提问是:CompletableFuture的cancel方法能中断线程的执行吗

        已知:我们不能直接使用thread.stop()这种非常强硬的手段来直接停止线程,风险很大,故提出了 中断interrupt(这里interrupt 和 stop的区别需要了解一下:java并发编程--- stop() 和 interrupt() 方法的主要区别_停止线程stop和interrupt的区别-CSDN博客

        但是中断 能直接让线程停止执行,从而去阻塞队列中重新获取任务吗?

        了解 stop 和 interrupt的小伙伴就知道了,中断仅是给线程一个标记,且线程完全可以不搭理这个标记依旧“我行我素”,那么上面的问题答案就出来了,没办法去中断一个任务的执行,除非线程任务有做相应的处理。

        当线程处于 WAITING、TIMED_WAITING 状态时,如果其他线程调用此线程的 interrupt() 方法,会使该线程返回到 RUNNABLE 状态,同时该线程的代码会触发 InterruptedException 异常

这里给一个demo:

public static Object longRunningMethod(Long time) {
    try {
        System.out.println("sleep start");
        thread = Thread.currentThread();
        Thread.sleep(time);
        System.out.println("sleep end");
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    System.out.println("目前状态:" + Thread.currentThread().getState());
    return "完成";
    }

    public static void main(String[] args) throws InterruptedException {
    // 创建一个新的CompletableFuture
    CompletableFuture<Object> future = CompletableFuture.supplyAsync(() -> {
        // 这里是要执行的方法
        return longRunningMethod(100L);
    });
    Thread.sleep(10);
    System.out.println(thread.getState());
    thread.interrupt();

    Thread.sleep(100000L);
}

后来我们采取的优化方式:

线程池给获取GPU任务独享,任务10s一次,这个时候利用 定时任务的思想就不要直接使用cron方式了,使用fixedDelay思想(@Scheduled中fixedDelay、fixedRate、initialDelay 和cron表达式的解析及区别_@scheduled initialdelay-CSDN博客

最后一次执行完时间 - > 下次开始时间

因为现在阻塞了就不发请求去获取就是最好的,如果GPU异常恢复了就会响应之前的请求,如果GPU异常没恢复,多发几个请求也基本上没用,线程资源因为阻塞也没办法释放

最终采用此方式:根据上次命令执行完毕时间 去推导 下次任务执行时间来达到优化此业务的目的

结语:

        之所以说上面使用Process.waitFor() 或 FuturnTask.waitFor不全符合现有场景,那是因为他们都是基于主线程(创建任务的线程)自身去阻塞然后唤醒以后检查 任务是否执行完

但是!任务该阻塞中依旧会在阻塞(也别想拉出对应线程去中断,麻烦且不优雅[做确实可以做~~~]),即使主线程给放开了,但是子线程依旧在阻塞,数量一多线程池又被干光了

一周后在《深入理解Java虚拟机》书中无意间看到一个优化案例,豁然开朗

最终优化方案:

        脚本里写个循环,获取到结果以后sleep 5秒然后再获取。而不用向现在这样在程序内间隔5秒去执行命令,避免fork(如果能找到java api也未尝不可)

Java中,线程池阻塞队列(BlockingQueue)的选择与设置是非常关键的,因为它们直接影响了系统的并发性能和资源管理。线程池中的阻塞队列用于存储任务,当线程池中的工作线程空闲时,会从队列中取出任务执行;反之,如果任务队列满了,新提交的任务会被阻塞,直到队列中有空间。 决定队列大小的因素通常包括: 1. **系统负载**:考虑应用程序的平均请求速率以及峰值负载。如果预计请求量波动较大,可能需要较大的缓冲能力来应对高峰期。 2. **硬件限制**:比如内存大小。队列过大会消耗过多内存,影响其他部分的性能。队列过小可能导致频繁地创建和销毁线程,增加上下文切换成本。 3. **任务类型**:对于I/O密集型任务,队列可以适当大一些,因为它不需要立即处理结果;而对于计算密集型任务,队列可能需要更小,以免CPU等待IO完成。 4. **超时策略**:如果你希望在队列满时设定一个时间限制(如生产者阻塞的时间),那么队列大小应该足够容纳这个超时时间内的任务。 常用的Java阻塞队列有`ArrayBlockingQueue`、`LinkedBlockingQueue`、`PriorityBlockingQueue`等,它们的配置参数`容量`就是指队列的最大元素数量。一般来说,初始容量可以设置为核心线程数的1.5到3倍,然后根据实际情况调整。同时,也可以设置无界队列(如`LinkedList`或自定义无限大小的队列),但这样可能会导致大量内存占用。 设置合适的队列大小需要进行实际监控和测试,找到一个既满足吞吐需求又能保证系统稳定性的平衡点。如果你提供具体的应用场景或数据,我可以给出更具体的建议。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值