• 线上线程池翻车总结


    项目描述:定时任务扫描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个小时,比之前提前了一个半小时。

  • 相关阅读:
    116-如何将java对象转换成json数据?
    082-redis的watch监听起到了什么作用?
    081-linux安装mysql时,为什么要使用mysql用户初始化mysql数据库?
    080-数据库的加密与解密起到了什么作用?
    115-linux安装redis
    114-汉字比较器
    114_LF will be replaced by CRLF 怎么解决?
    113-github如何删除某个文件和文件夹?
    112-git如何向服务器github上传文件
    学习笔记(31)-python访问ElasticSearch
  • 原文地址:https://www.cnblogs.com/yangyongjie/p/12508820.html
Copyright © 2020-2023  润新知