线程池任务问题的深入思考

问题描述

每次执行一组任务,一组任务最多有 15 个,多线程执行,每个线程处理一个任务;每次执行完一组任务后,再执行下一组,不存在上一组的任务和下一组一起执行的情况

代码重现
  1. 定义线程池
    在这里插入图片描述
  2. 重现代码
  @Test
    public void contextLoads() throws Exception {

        // 一共 10 批任务
        for(int i = 0; i < 10; i++) {
            // 每次执行一批任务
            doOnceTasks();
            System.out.println("---------------------------------------" + i);
        }
    }
  /**
     * 每次完成 15 个任务后,再进行下一次任务
     * -->理论上 14 个核心线程+1 个阻塞队列即可完成一组任务,连非核心线程都无需使用,为什么会出现线程被占满的情况
     */
    private void doOnceTasks() {
        List<Future> futureList = Lists.newArrayListWithCapacity(15);
        for(int i = 0; i < 15; ++i){
            Future future = executor.submit(()->{
                // 随机睡 0-5 秒
                int sec = new Double(Math.random() * 5).intValue();
                LockSupport.parkNanos(sec * 1000 * 1000 * 1000);
                System.out.println(Thread.currentThread().getName() + "  end");
//                countDownLatch.countDown();
            });
            futureList.add(future);
        }
//        // 等待所有任务执行结束
        for(Future future : futureList){
            try {
                future.get();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

问题定位

  1. 错误堆栈信息展示
    在这里插入图片描述
  2. 根据堆栈信息定位相应的代码
    在这里插入图片描述
  3. 通过代码发现是因为超过最大线程数,触发拒绝策略

问题分析

在这里插入图片描述

  1. 为什么会创建非核心线程?
    1.1 业务线程任务封装在Worker的firstTask属性,直接调用其run方法触发,但不会清除线程(见下第二批执行任务前的截图-14个线程处于阻塞等待)
    (1)所以阻塞取业务线程结果,不会清除线程
    1.2 Worker执行初始化的任务和循环取队列中的任务执行(见下runWorker截图)
    1.3 第二批之后任务都是基于队列的生产与消费
    (1)LinkedBlockingQueue#take 方法,如果队列已空,则所有取元素的线程会阻塞在一个 Lock 的 notEmpty 等待条件上;等有元素入队时,只会调用 signal 方法唤醒一个线程取元素,而不是所有线程
    (2) 提交任务的速度比线程从阻塞队列取任务的速度快,进而导致创建非核心线程执行任务
    在这里插入图片描述
    在这里插入图片描述

解决方式

使用SynchronousQueue
  1. 使用 SynchronousQueue,即阻塞队列大小设置为 0
  2. SynchronousQueue 是根据是否有等待线程而决定是否入队成功,而 LinkedBlockingQueue 是根据缓冲区,而不管是否已经有等待线程
    2.1 SynchronousQueue
    在这里插入图片描述
    2.2 LinkedBlockingQueue
    在这里插入图片描述
根据业务情况配置阻塞队列

对于上述案例,因为任务最多只有15个,将阻塞队列大小设置为15,就保证了不会出现任务被拒绝

补充

ThreadPool ctl 变量的设计

在这里插入图片描述

  1. ThreadPool ctl变量是用int的高3位 + 29个0代表状态,用高位000+低29位来表示线程池中工作线程的数量
    1.1 int COUNT_BITS = Integer.SIZE - 3 // 32 -3 =29
    1.2 int CAPACITY = (1 << COUNT_BITS) - 1
    (1)执行返回536870911(10进制)
    (2)10进制转换为二进制(29个1)
怎样销毁线程(待下次详细分析)

在这里插入图片描述

  1. 实现思路:如果 getTask 返回null, 则结束循环,该 worker 线程将被销毁
  2. getTask 在什么情况下会返回 false?
    2.1 如果线程池的状态为SHUTDOWN并且队列不为空
    2.2 如果线程池的状态大于STOP
    2.3 如果当前运行的线程数大于最大线程数
    在这里插入图片描述
对提交任务分析(重点关注addWorker方法)
  1. addWorker前面流程简要概括
    在这里插入图片描述
  2. CAS增加线程数
    在这里插入图片描述
  3. 封装业务线程任务,启动线程
    在这里插入图片描述
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值