线上线程池翻车总结

项目描述:定时任务扫描300万条数据库记录,并对每条记录执行检查操作(调用其他服务接口,发送短信等)。

版本迭代记录:

  1)第一版:一次查询全部300万条数据放在JVM内存中,没有使用线程池,使用固定20个线程,每个线程循环不停的从内存中取一条数据执行,直到所有数据全部执行完为止。

    这种方式耗时大约3个半小时,代码如下:

 // 实现runnable接口的线程,infos:全部300条任务
            CheckTask checkTask = new CheckTask(infos);
            // 固定20个线程执行,线程不能回收再利用(线程池可以)
            for (int i = 0; i < 20; i++) {
                Thread t = new Thread(checkTask, name + "_" + i);
                t.start();
            }

    private class CheckTask implements Runnable {
        private List<Info> infos;
        private volatile int count = 0;

        public CheckTask(List<Info> infos) {
            this.infos= infos;
        }

        @Override
        public void run() {
            boolean done = false;
            while (!done) {
                try {
                    Info info = null;
                    // 使用同步锁
                    synchronized (infos) {
                        if (count < infos.size()) {
                            info = infos.get(count);
                            count++;
                        } else {
                            done = true;
                            break;
                        }
                    }

                    if (info != null) {
                        // 对取到的一个任务执行业务逻辑
                        ...
                    }
                } catch (Exception e) {
                    // 异常处理
                }
            } // while
        }
    }

  2)第二版:翻车的版本,使用线程池,线程池核心线程数为1,最大线程数为50,等待队列为1000。分页查询300万条任务数据,每次执行1000条任务,使用CountDownLatch控制当前页1000条执行完毕再执行下一页。

    翻车原因:核心线程数为1,等待队列为1000,每次执行的任务数为1000;根据线程池的执行规则,线程池中的线程数达到核心线程数1之后,将剩余的任务全部放在了队列中,因为队列很大,所以线程池中根本没有

  再创建新的线程,只有一个核心线程在慢慢地循环反复从队列中取任务去执行。根据日志估算一个线程执行完一个任务需要0.25秒,执行完300万个任务需要208小时,比之前的三个半小时增加了近7倍,失败。

    翻车代码: 

    /**
     * 创建一个用于发送邮件的线程池,核心线程数为1,最大线程数为100,线程空闲时间为60s,
     */
    private static final ThreadPoolExecutor EXECUTORS = new ThreadPoolExecutor(1, 100, 60, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>(1000), new CustomThreadFactory("checkTask"), new RejectedExecutionHandler() {
        @Override
        public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
            // 打印日志,并且重启一个线程执行被拒绝的任务
            LOGGER.error("Task:{},rejected from:{}", r.toString(), executor.toString());
            // 直接执行被拒绝的任务,JVM另起线程执行
            r.run();
        }
    });
            
            int pageSize = 1000;
            for (int i = 1; i <= pageCount; i++) {
                PageInfo pageInfo = new PageInfo((i-1)*pageSize, pageSize);
                // 查询到一页然后进行处理
                List<Info> infos = infoDao.queryInfoByPage(infoVo, pageInfo);
                // 同步执行这一页
                // 当前页的大小
                int size = infos.size();
                final CountDownLatch countDownLatch = new CountDownLatch(size);
                for (final Info info : infos) {
                    // 交给线程池去执行
                    EXECUTORS.execute(new MDCRunnable(new Runnable() {
                        @Override
                        public void run() {
                            try {
                                // 执行业务逻辑
                               ...
                            } catch (Exception e) {
                                LOGGER.error( "检查异常" + e.getMessage(), e);
                            } finally {
                                // 无论执行结果如何都要countDown,避免影响后续的检查
                                countDownLatch.countDown();
                            }
                        }
                    }));
                }
                // 等待线程池执行完一页的自动续费检查
                try {
                    countDownLatch.await();
                } catch (InterruptedException e) {
                    LOGGER.error("await一页检查异常" + e.getMessage(), e);
                }
            }                

  3)第三版:对线程池进行调优

    已知线上服务器cpu为16核,堆内存最大可为64G。第二版得知一个线程执行一个任务大约需要0.25秒,所以在每页1000个任务不变的情况下对线程池进行了优化。

    

   (1)方案一:

    1000个任务1秒内会全部丢进线程池,40个线程1秒处理完160个任务,100个线程每秒处理400个任务,所以1000个任务全部到来,核心线程处理不完,将600个任务在队列等待,400个任务在执行,即使被拒绝也会另起线程执行。

    在开发环境i7 12核处理器,JVM内存大小为750m 测试,执行100万个任务耗时大约2500秒。

    线程池代码:

    /**
     * 创建一个用于发送邮件的线程池,核心线程数为40,最大线程数为100,线程空闲时间为60s
     * 服务器CPU为16核,堆内存大小为最大64g
     * 经测试一个任务执行大概耗时250ms,0.25s,40个线程每秒钟执行大约160个任务,100个线程每秒钟执行400个任务
     */
    private static final ThreadPoolExecutor EXECUTORS = new ThreadPoolExecutor(40, 100, 60, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>(600), new CustomThreadFactory("autoRenewCheckTask"), new RejectedExecutionHandler() {
        @Override
        public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
            // 打印日志,并且重启一个线程执行被拒绝的任务
            LOGGER.error("Task:{},rejected from:{}", r.toString(), executor.toString());
            // 直接执行被拒绝的任务,JVM另起线程执行
            r.run();
        }
    });

    结果:又翻车了,执行1000条任务大概需要9秒左右,远超设想的2秒。

    分析:

   (2)方案二:在第一版的基础上,将固定线程数调至30,其他不变。

    结果:执行完300万条任务,耗时2个小时,比之前提前了一个半小时。

原文地址:https://www.cnblogs.com/yangyongjie/p/12508820.html