多线程系列:附、LinkedBlockingQueue 引发的一次线上事故

总结:

   BlockingQueue 在通知空闲线程获取任务时,是通过信号量的方式,每次唤醒一个线程去取任务,所以,当向线程池提交任务速率 > BlockingQueue 通知线程取任务速率时(即每次唤醒一个线程去执行任务处理不过来),新提交的任务便会执行对应的拒绝策略。

业务场景:

   对账进程需要下载 某一时间段n个账号 的第三方账单,第三方账单中数据量大,需要控速入库,入库完一组数据,再去下载第二组账单。

线程池配置:

@Bean
public ThreadPoolTaskExecutor cpuThreadPool() {
    ThreadPoolTaskExecutor poolTaskExecutor = new ThreadPoolTaskExecutor();
    poolTaskExecutor.setCorePoolSize(4);
    poolTaskExecutor.setMaxPoolSize(5);
    poolTaskExecutor.setQueueCapacity(2);
    poolTaskExecutor.setKeepAliveSeconds(60);
    poolTaskExecutor.setAllowCoreThreadTimeOut(false);

    poolTaskExecutor.setWaitForTasksToCompleteOnShutdown(true);
    poolTaskExecutor.setAwaitTerminationSeconds(20);
    return poolTaskExecutor;
}

问题描述:

Demo:


@RunWith(SpringRunner.class)
@SpringBootTest
public class ThreadPoolInSpring {
    @Resource
    private ThreadPoolTaskExecutor cpuThreadPool;

    @Test
    public void contextLoads() throws Exception {
        // 下载十天的账单
        for (int i = 0; i < 10; i++) {
            // 每次执行一批任务
            doOnceTasks(i);
            System.out.println("第" + i + "天的下载");
        }
    }

    /**
     * 每次处理完3个账号的数据后,再进行下一天的任务
     * @param day
     */
    private void doOnceTasks(int day) {
        int num=5;
        List<Future> futureList = Lists.newArrayListWithCapacity(num);
        for (int i = 0; i < num; ++i) {
            int finalI = i;
            Future future = cpuThreadPool.submit(() -> {
                // 随机睡 0-5 秒
                int sec = new Double(Math.random() * 5).intValue();
                LockSupport.parkNanos(sec * 1000 * 1000 * 1000);
                System.out.println(Thread.currentThread().getName() +"第" + day + "天的第 "+ finalI +"个下载 "+ "  end");
            });
            futureList.add(future);
        }

        // 等待所有任务执行结束
        for (Future future : futureList) {
            try {
                future.get();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
}

   执行下载任务时,出现 RejectedExecutionException 异常,默认情况下 ThreadPoolTaskExecutor 使用 AbortPolicy 拒绝策略,可以明确是因为向线程池提交了过多的任务。

原因分析:

java.util.concurrent.ThreadPoolExecutor#execute

public void execute(Runnable command) {
        if (command == null)
            throw new NullPointerException();
        /*
         * Proceed in 3 steps:
         *
         * 1. If fewer than corePoolSize threads are running, try to
         * start a new thread with the given command as its first
         * task.  The call to addWorker atomically checks runState and
         * workerCount, and so prevents false alarms that would add
         * threads when it shouldn't, by returning false.
         *
         * 2. If a task can be successfully queued, then we still need
         * to double-check whether we should have added a thread
         * (because existing ones died since last checking) or that
         * the pool shut down since entry into this method. So we
         * recheck state and if necessary roll back the enqueuing if
         * stopped, or start a new thread if there are none.
         *
         * 3. If we cannot queue task, then we try to add a new
         * thread.  If it fails, we know we are shut down or saturated
         * and so reject the task.
         */
        int c = ctl.get();
        if (workerCountOf(c) < corePoolSize) {
        	//添加核心线程
            if (addWorker(command, true))
                return;
            c = ctl.get();
        }
        //任务入阻塞队列
        if (isRunning(c) && workQueue.offer(command)) {
            int recheck = ctl.get();
            if (! isRunning(recheck) && remove(command))
            	//线程池已被关闭 且 从阻塞队列中移除任务成功
                reject(command);
            else if (workerCountOf(recheck) == 0)
                addWorker(null, false);
        }
        //执行到这里时,表示需要添加非核心线程了(参数false)
        else if (!addWorker(command, false))
        	//非核心线程创建失败
            reject(command);
    }

   调试上面的源码,Demo 进入的是第二个 reject 方法,我们继续跟踪,发现是因为线程池中的线程数量超过了 maximumPoolSize,所以执行了 reject 方法。

为什么任务入阻塞队列会失败 ?

在这里插入图片描述
   查看源码可知,workqueue 在该业务中是 LinkedBlockingQueue( QueueCapacity > 0 ) ,所以我们需要去阅读 LinkedBlockingQueue 是如何处理提交和移除任务的。

java.util.concurrent.LinkedBlockingQueue#signalNotEmpty
/** Wait queue for waiting takes */
private final Condition notEmpty = takeLock.newCondition();
    
private void signalNotEmpty() {
        final ReentrantLock takeLock = this.takeLock;
        // 加锁
        takeLock.lock();
        try {
        // 唤醒一个 take 线程
            notEmpty.signal();
        } finally {
            takeLock.unlock();
        }
    }

  可以看到,此处只是用 Condition 对象唤醒了一个空闲线程去执行任务,而不是所有线程。

  我们阻塞队列只有 2,通过我们线程池的配置参数,每次批任务(5 个)必定提交一个到队列中。主线程不断的提交任务到线程池,线程池的队列异步的唤醒一个线程,并取一个任务。

  所以我们可以知道,提交任务的速度线程从阻塞队列取任务 的速度快,进而导致创建非核心线程执行任务,最终的结果就是:在多批任务之后,再无非核心线程可创建,导致执行拒绝策略。流程入下图所示:
在这里插入图片描述

解决方案:

  本次问题是由 BlockingQueue 唤醒线程从阻塞队列取任务慢导致的,所以我们可以使用 SynchronousQueue 直接将任务提交给 等待的线程,避免出现这种情况。

思考:

1. Condition 对象是如何唤醒一个线程的?

   Condition 是重入锁的伴生对象,它提供了在重入锁的基础上,进行等待(await())和通知(signal())的机制,更多相关的内容后期会附上链接。

2. 从业务和线程池参数来看,一定有线程是空闲的,为什么没使用到空闲线程来执行任务?而是执行了拒绝策略?

   从 execute(Runnable command) 源码中我们可以看到,只有 BlockingQueue 会去唤醒空闲线程执行任务,而 BlockingQueue 每次只会唤醒一个线程,先等待分配资源,再去执行新任务,所以向线程池提交任务速率比唤醒线程执行任务速率大时,execute 方法中就会对新任务执行拒绝策略。

线程池任务问题处理历程

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
JAVA线程基本学习, JAVA多线程的特性= 线程池: 本质上是一个对象池, 用来管理线程资源. 在任务执行前, 需要从线程池中拿出线程来执行. 在任务执行完成之后, 需要把线程放回线程池. 线程池好处: 降低资源的消耗, 线程本身是一种资源, 创建和销毁都会消耗CPU内存, 频繁的创建和销毁会浪内存. 提高任务执行的响应速度, 任务执行时, 可以不必等线程创建完成之后在执行,可以直接获取线程执行任务. 提高线程的可管理性, 线程不能无限制的创建, 需要进行统一的分配, 调优和监控. 不适用线程池坏处: 频繁的线程创建和销毁会占用更多的CPU和内积. 频繁的创建和销毁会对gc产生比较大的压力. 线程太多,线程切换带来的开销将不可忽视. 线程太少, 多核CPU得不到充分利用, 浪费资源. 线程池实现原理: 分为三部分: 核心线程池 线程池 队列 拒绝策略 主要流程: 1.判断核心线程池是否已满, 如果不是, 则创建线程执行任务 2.如果核心线程池满了, 判断队列是否满了, 如果队列没满, 将任务放在队列中 3.如果队列满了, 则判断线程池是否已满, 如果没满, 创建线程执行任务 4.如果线程池满了, 按照拒绝策略对任务进行处理 JDK中提供了一个线程池工厂: Executors ,很多工厂方法, 可以创建多种线程池 1.单一线程池 ExecutorService newSingleThreadExecutor = Executors.newSingleThreadExecutor(); 该线程池只有一个线程, 若多个任务被提交到此线程池, 那么会被缓冲到队列,当线程空闲时,按照FIFO的方式进行处理. 2.固定数量线程池 ExecutorService newFixedThreadPool = Executors.newFixedThreadPool(5); 和创建单一线程池类似, 不同的是线程池中有多个线程, 可以并行处理任务, 若多个线程任务被提交到此线程池, 会有以下执行过程: 如果线程的数量未达到指定数量, 则创建新线程执行任务. 如果线程池的数量达到了指定数量, 而且此时有线程是空闲的, 则取出空闲线程来执行任务. 如果没有线程是空闲的, 则将任务缓冲到队列, 当线程空闲的时候, 按照FIFO的方式进行处理. FIFO: 先进先出 3.带缓冲的线程池 该线程池中, 核心线程池长度为0, 线程池最大长度为Integer.MAX_VALUE.以SynchronousQueue作为等待队列, 从而每次往队列中插入一个元素, 必须等待另一个线程从这个队列删除一个元素. 定时调度 4.定时调度的线程池 ExecutorService newCachedThreadPool = Executors.newScheduledThreadPool(); ScheduledExecutorService newScheduledThreadPool = Executors.newScheduledThreadPool(5); ExecutorService newWorkStealingPool = Executors.newWorkStealingPool() 线程池家族 线程池的顶层接口是Executor, 这个接口定义了一个核心方法executor(Runnable command), 这个方法最后被ThreadPoolExecutor类实现, 这个方法用来传入任务, 并且该类是线程池的核心类, 构造方法如下 : public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,BlockingQueue<Runnable> workQueue); public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,BlockingQueue<Runnable> workQueue,ThreadFactory threadFactory); public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,BlockingQueue<Runnable> workQueue,RejectedExecutionHandler handler); public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,BlockingQueue<Runnable> workQueu, ThreadFactory threadFactory,RejectedExecutionHandler handler); 参数意义: CorePoolSize: 核心线程池大小, 如果核心线程池有空闲的位置, 新的任务就会被核心线程池新建一个线程执行, 执行完毕不会销毁线程, 线程会进入缓冲队列等待再次被运行 MaximunPoolSize: 线程池能创建最大的线程数量, 如果核心线程池和缓冲队列都已经满了, 新的任务就会进来创建新的线程来执行, 但是数量不能超过maximunPoolSize, 否则采取拒绝接受任务策略 KeepAliveTime: 非核心线程能够空闲的最长时间, 超过时间, 线程终止, 这个参数默认只有在线程数量超过核心线程池大小时, 才会起作用. Unit: 时间单位, 和keepAliveTime配合使用. WorkQueue: 缓冲队列, 用来存放等待被执行的任务 ThreadFactory: 线程工厂, 用来创建线程, 一般有三个选择 ArrayBlockingQueue LinkedBlockingQueue SynchronousQueue Handler :拒绝处理策略, 线程数量大于最大线程数量就会拒绝处理策略, 四种策略为 ThreadPoolExecutor.AbortPolicy:丢弃任务并抛出RejectedExecutionException异常。 ThreadPoolExecutor.DiscardPolicy:也是丢弃任务,但是不抛出异常。 ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列最前面的任务,然后重新尝试执行任务(重复此过程) ThreadPoolExecutor.CallerRunsPolicy:由调用线程处理该任务 Executor接口有一个子接口ExecutorService, ExecutorService的实现类AbstracExecutorService, 而ThreadPoolExecutor正是AbstracExecutorService的子类. ThreadPoolExecutor还有两个常用的方法shutdown和submit,两者都用来关闭线程池, 但是submit有一个结果返回. 线程池任务执行 当执行executor(Runnable command)方法后, 传入一个任务, public void execute(Runnable command) { if (command == null) throw new NullPointerException(); if (poolSize >= corePoolSize || !addIfUnderCorePoolSize(command)) { if (runState == RUNNING && workQueue.offer(command)) { if (runState != RUNNING || poolSize == 0) ensureQueuedTaskHandled(command); } else if (!addIfUnderMaximumPoolSize(command)) reject(command); } } (传入的对象实现了Runnable接口, 这就解释了: 为什么实现多线程的方式中继承Thread类不能使用线程池了) 执行过程: 首先判断任务是否为空, 为空抛出空指针异常, 否则执行下一个判断: 当前线程数量是否小于核心线程池线程数量, 是,则执行addIfUbderCorePollSize(command)方法, 在核心线程池中创建新的线程, 并且执行这个任务 总结: 如果当前线程池中的数目小于corePollSize, 则每来一个任务, 就会创建一个新的线程去执行这个任务 如果当前线程池中的线程数目>=corePollSize, 则每来一个任务, 会尝试将其添加到缓冲队列中, 如果添加成功, 则该任务会等待空闲线程将其取出去执行, 如果添加失败(一般是以为任务队列已经满了), 则会尝试创建新的线程去执行这个任务. 如果当前线程池中的线程数目达到maximunPoolSize, 则会采取任务拒绝策略进行处理. 如果线程池中的线程数量大于corePoolSize时, 如果某线程空闲时间超过keepAliveTime, 线程将被终止, 直至线程池中的线程数目小于等于corePoolSize; 如果允许核心池中的线程设置存活时间, 那么核心池中的线程空闲时间超过keepAliveTime, 线程也会被终止.

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值