V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
frank1256
V2EX  ›  Java

Java 线程池使用 Future,任务没完成啥意思

  •  
  •   frank1256 · 2022-06-07 17:24:08 +08:00 · 2797 次点击
    这是一个创建于 933 天前的主题,其中的信息可能已经有所发展或是发生改变。

    线程池,核心数 10 个,我循环 2 次,等待第一次 10 个线程结束,无法立刻提交第二次的 10 个线程? 同一时刻只会有 10 个线程跑,咋回事啊,大佬指点迷津 提示的 Not completed,难道 call()方法执行完毕,不算一个任务完成吗

    代码如图

    报错

    finished100
    finished100
    finished100
    finished100
    finished100
    finished100
    finished100
    finished100
    finished100
    finished100
    finished one repeate
    Exception in thread "main" java.util.concurrent.RejectedExecutionException: Task java.util.concurrent.ExecutorCompletionService$QueueingFuture@48140564[Not completed, task = java.util.concurrent.Executors$RunnableAdapter@7c30a502[Wrapped task = java.util.concurrent.FutureTask@1d251891[Not completed, task = executor.ExecutorDemo$1@49e4cb85]]] rejected from java.util.concurrent.ThreadPoolExecutor@2133c8f8[Running, pool size = 10, active threads = 2, queued tasks = 0, completed tasks = 10]
    	at java.base/java.util.concurrent.ThreadPoolExecutor$AbortPolicy.rejectedExecution(ThreadPoolExecutor.java:2055)
    	at java.base/java.util.concurrent.ThreadPoolExecutor.reject(ThreadPoolExecutor.java:825)
    	at java.base/java.util.concurrent.ThreadPoolExecutor.execute(ThreadPoolExecutor.java:1355)
    	at java.base/java.util.concurrent.ExecutorCompletionService.submit(ExecutorCompletionService.java:184)
    	at executor.ExecutorDemo.main(ExecutorDemo.java:34)
    
    
    
    第 1 条附言  ·  2022-06-08 11:26:18 +08:00
    评论已有解答,主要是第二批提交任务时,队列进了 1 个,但是需要 1 秒的耗时,导致继续提交的时候,核心线程用完了,队列满了,最大线程一样是 10,就拒绝了
    25 条回复    2022-06-08 12:34:41 +08:00
    gengzi
        1
    gengzi  
       2022-06-07 17:39:17 +08:00
    跟你配置的线程池参数有关系,核心和最大线程数都是 10 ,阻塞队列为 1 ,当前线程数大于 maxpoolsize ,并且阻塞队列已满,就走拒绝策略了,默认的拒绝策略应该是 抛异常
    frank1256
        2
    frank1256  
    OP
       2022-06-07 17:44:45 +08:00
    @gengzi 我在内部循环的时候只提交了 10 次,只会有 10 个线程存在,然后用 future.get 阻塞,直到 10 个线程完成任务.此时阻塞队列里任务 0,第二次循环的,还是提交了 10 个任务,应该还是由一开始的 10 个线程完成才对. 整个流程在同一时刻只会有 10 个任务
    q1angch0u
        3
    q1angch0u  
       2022-06-07 17:50:40 +08:00
    使用 ArrayBlockingQueue 有界任务队列,若有新的任务需要执行时,线程池会 [创建] 新的线程,直到 [创建] 的线程数量达到 corePoolSize 时,则会将新的任务加入到等待队列中。若等待队列已满,即超过 ArrayBlockingQueue 初始化的容量,则继续创建线程,直到线程数量达到 maximumPoolSize 设置的最大线程数量,若大于 maximumPoolSize ,则执行拒绝策略。注意是创建线程,旧线程是不会复用的,因为你的 corePollSize 是 10 ,所以第一次建的 10 个线程是不会被销毁的。
    q1angch0u
        4
    q1angch0u  
       2022-06-07 17:52:11 +08:00
    @frank1256 #2 threadPoolExecutor 是线程池啊,放线程的地儿,一开始的 10 个和第二波的 10 个并不是相同的线程,为啥会 [由一开始的 10 个线程] 完成呢? 0-9 和 10-19 是没关系的。
    q1angch0u
        5
    q1angch0u  
       2022-06-07 17:52:49 +08:00
    你可以把 corePoolSize 改成 0 试试,刚开始的 10 个会超时回收,应该就没问题了。
    frank1256
        6
    frank1256  
    OP
       2022-06-07 17:58:04 +08:00
    @q1angch0u 额.可是我希望的是 20 个任务,都是由一开始创建的 10 个线程来完成. corePoolSize 设 0,是会再建 10 个,但我已经有 10 个存活了,我没必要去在建 10 个额
    twinsdestiny
        7
    twinsdestiny  
       2022-06-07 17:59:26 +08:00
    @frank1256 那就不要把 keepAaliveTime 设置为 0
    q1angch0u
        8
    q1angch0u  
       2022-06-07 17:59:46 +08:00 via iPhone
    @frank1256 #6 那你就不应该再新建 10 个线程,前十个线程叫小王,后十个线程叫小红,小红怎么能叫小王呢…
    frank1256
        9
    frank1256  
    OP
       2022-06-07 18:01:45 +08:00
    @q1angch0u 我没再建 10 个线程啊,提交到线程池就新建一个线程了吗?不先检查已经建的线程是否空闲吗?
    q1angch0u
        10
    q1angch0u  
       2022-06-07 18:05:51 +08:00 via iPhone
    @frank1256 #9 第一问:是的,submit 就是新建线程;第二问:线程空闲了啊,但是它目前活着的比 corePoolSize 小,所以不会销毁呀。
    frank1256
        11
    frank1256  
    OP
       2022-06-07 18:08:22 +08:00 via iPhone
    @q1angch0u 😨
    frank1256
        12
    frank1256  
    OP
       2022-06-07 18:12:11 +08:00 via iPhone
    @q1angch0u 没有方法,让我的第二次 10 个任务依然由第一次建的 10 个线程执行吗?
    q1angch0u
        13
    q1angch0u  
       2022-06-07 18:12:40 +08:00 via iPhone
    @frank1256 或者也可以设置 allowCoreThreadTimeout=true
    q1angch0u
        14
    q1angch0u  
       2022-06-07 18:13:24 +08:00 via iPhone
    @frank1256 #12 有办法让小红变成小王吗……
    frank1256
        15
    frank1256  
    OP
       2022-06-07 18:21:58 +08:00 via iPhone
    @q1angch0u 好吧,我得再试试
    q1angch0u
        16
    q1angch0u  
       2022-06-07 18:35:03 +08:00 via iPhone
    @frank1256 对不起,我好像误人子弟了。
    evilnull
        17
    evilnull  
       2022-06-07 18:52:57 +08:00
    线程池线程的第一个任务是线程创建时直接分配的,之后所有的任务是从队列里获取的。
    你第二次提交 10 个任务时,核心线程已满,不会再创建核心线程,任务会先提交到队列里,核心线程会从队列里取任务去执行。由于你队列长度是 1 ,而且任务提交速度比线程从队列里取任务速度快,会有部分任务提交时队列是满的,最终走到拒绝策略。
    可以将线程池队列长度调大点,或者使用 SynchronousQueue 。
    frank1256
        18
    frank1256  
    OP
       2022-06-07 18:59:00 +08:00 via iPhone
    frank1256
        19
    frank1256  
    OP
       2022-06-07 18:59:45 +08:00 via iPhone
    @evilnull 好像是这么回事
    frank1256
        20
    frank1256  
    OP
       2022-06-07 19:00:13 +08:00 via iPhone
    @evilnull 谢谢
    wbd31
        21
    wbd31  
       2022-06-07 19:03:56 +08:00
    线程池的核心线程是不受 keepAliveTime 参数控制的,所以 0-10 和 11-20 都会被同一批核心线程执行。
    因为 workQueue 设置的容量是 1 ,当第二批任务开始执行的时候,先插入到 workQueue, 此时核心线程执行完了自己的 firstTask 后会一直轮询 getTask 获取 workQueue 里的任务,但是任务里 sleep 了 1s 导致消费速度小于第二批任务的 submit 速度(其实就算不 sleep 也有可能发生)导致 submit 的时候进入到了 addWorker 的判断,由于已经达到了最大线程数量,不再新建线程,走了 reject
    frank1256
        22
    frank1256  
    OP
       2022-06-08 09:03:14 +08:00
    @wbd31 了解了
    byte10
        23
    byte10  
       2022-06-08 11:19:00 +08:00
    因为核心线程数设置了 10 ,跟最大线程数 10 一样,因为内置的线程池实现是默认不初始化核心线程数的( apache 默认初始化核心线程数),所以第一次时候,全部使用创建新线程执行 10 个任务。

    到了第二次之后,任务来了之后默认存放在队列中(因为 corePoolSize 已经满了),但是这个时候太快了,线程池还没来得及消费这个队列任务(还没拿走这个任务),就满了(而且最大线程数也等于 10 ,也满了),导致触发拒绝策略。我也纳闷为何不直接拿核心线程去执行呢?因为去拿核心线程执行,还需要判断这个核心线程是否空闲,那不如直接丢到任务队列里,核心线程有空闲就直接去拿就可以了。

    尝试调整参数,调整 corePoolSize = 7 或者 8 , maximumPoolSize=10 不变,还是不行。前面 10 个任务正常处理,但是到了第 14, 15 个依然不行。虽然 keepAliveTime = 0, 但是还没来得及释放线程,就会出现问题队列塞满的情况,corePoolSize = 0 或者 1 ,就可以正常执行,因为来得及释放线程,后续的任务依然可以新创建线程来执行。

    这种极端的测试还是挺有意思的。可以先了解下 线程池原理,因为很快找到问题。
    byte10
        24
    byte10  
       2022-06-08 11:33:15 +08:00
    @frank1256 对了 解决的办法是:任务等待队列适当增加,一般按能接受的任务执行时长来设置。如果很大,意味着后续的任务可能要排队很久才会被执行到,好比去银行排队,前面都有 1000 人 ,实在没必要排队。这个时候应该抛出异步,早点暴露出当前线程池不足的问题。而你的这种情况 新增任务太快,可以适当延迟 1-3ms 的新增间隔,让线程池有时间去取队列中的任务即可,或者设置队列长度 5-10 即可。你这种情况毕竟比较极端的测试,实际场景较少。keepAliveTime 也还好,10-60s 都可以,跟任务并发吞吐量 的变化有一些关系,一般不是特别需要关注。0 的话太极端了。
    nothingistrue
        25
    nothingistrue  
       2022-06-08 12:34:41 +08:00
    我看了回复才发现你任务队列长度是 1 ,那一次性提交 10 个任务,出错是正常的,不出错才有问题。

    这个 ThreadPoolExector ,Executor 执行器才是关键,ThreadPool 线程池只是执行器的一种实现方式(虽然 Java 就这一种实现方式,但其他语言会有其他方式)。

    然后你的问题,你已经回答了,call()方法执行完毕,确实不算一个任务完成。service.take()执行完成后,可以确定 call()方法执行完了,通常也代表任务执行完了,但是执行器内部有没有把任务标记为完成,就不好说了,一般会有少量的延迟。

    最后,我看了一下你的错误信息,里面有这么一句:

    .ThreadPoolExecutor@2133c8f8[Running, pool size = 10, active threads = 2, queued tasks = 0, completed tasks = 10]

    注意这个 active threads = 2 ,第一个 10 个任务完成之后,执行器有可能做优化了,就留 2 个线程,毕竟你队列长度是 1 ,2 个线程足够了。
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   2846 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 21ms · UTC 14:57 · PVG 22:57 · LAX 06:57 · JFK 09:57
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.