线程池实战应用

线程池的设置

corePoolSize:核心线程数

核心线程会一直存活,即使没有任务需要执行

当线程数小于核心线程数时,即使有线程空闲,线程池也会优先创建新线程处理

设置allowCoreThreadTimeout=true(默认false)时,核心线程会超时关闭

(1)高并发、任务执行时间短的业务,线程池线程数可以设置为CPU核数+1,减少线程上下文的切换
(2)并发不高、任务执行时间长的业务要区分开看:
a)假如是业务时间长集中在IO操作上,也就是IO密集型的任务,因为IO操作并不占用CPU,所以不要让所有的CPU闲下来,可以加大线程池中的线程数目,让CPU处理更多的业务
b)假如是业务时间长集中在计算操作上,也就是计算密集型任务,这个就没办法了,和(1)一样吧,线程池中的线程数设置得少一些,减少线程上下文的切换
(3)并发高、业务执行时间长,解决这种类型任务的关键不在于线程池而在于整体架构的设计,看看这些业务里面某些数据是否能做缓存是第一步,增加服务器是第二步,至于线程池的设置,设置参考(2)。最后,业务执行时间长的问题,也可能需要分析一下,看看能不能使用中间件对任务进行拆分和解耦

tasks :每秒的任务数,假设为500~1000
taskcost:每个任务花费时间,假设为0.1s
responsetime:系统允许容忍的最大响应时间,假设为1s
做几个计算
corePoolSize = 每秒需要多少个线程处理?
threadcount = tasks/(1/taskcost) =tasks*taskcout = (500~1000)*0.1 = 50~100 个线程。corePoolSize设置应该大于50
根据8020原则,如果80%的每秒任务数小于800,那么corePoolSize设置为80即可

queueCapacity:任务队列容量(阻塞队列)

最常使用的两种队列
ArrayBlockingQueue

是一个基于数组结构的有界阻塞队列,底层结构是一个数组

LinkedBlockingQueue

一个基于链表结构的有界阻塞队列(不设置大小时,默认为Integer.MAX_VALUE),底层结构是一个单向链表

当核心线程数达到最大时,新任务会放在队列中排队等待执行,如果长度设置的不合理就无法发挥出多线程的威力。队列默认长度是int最大值,
队列长度设置此长度,那么线程池个数将只会增加到corePoolSize
,如果corePoolSize个数设置又过小,这样就会无法发挥出多线程的威力。

Tomcat的线程池队列是无限长度的,但是线程池会一直创建到maximumPoolSize,然后才把请求放入等待队列中

tomcat 任务队列org.apache.tomcat.util.threads.TaskQueue其继承与LinkedBlockingQueue,覆写offer方法。

    @Override
    public boolean offer(Runnable o) {
      //we can't do any checks
        if (parent==null) return super.offer(o);
        //we are maxed out on threads, simply queue the object
        if (parent.getPoolSize() == parent.getMaximumPoolSize()) return super.offer(o);
        //we have idle threads, just add it to the queue
        if (parent.getSubmittedCount()<(parent.getPoolSize())) return super.offer(o);
         //线程个数小于MaximumPoolSize会创建新的线程。
        //if we have less threads than maximum force creation of a new thread
        if (parent.getPoolSize()<parent.getMaximumPoolSize()) return false;
        //if we reached here, we need to add it to the queue
        return super.offer(o);
    }

queueCapacity = (coreSizePool/taskcost)responsetime
计算可得 queueCapacity = 80/0.1
1 = 80。意思是队列里的线程可以等待1s,超过了的需要新开线程来执行
切记不能设置为Integer.MAX_VALUE,这样队列会很大,线程数只会保持在corePoolSize大小,当任务陡增时,不能新开线程来执行,响应时间会随之陡增。

maxPoolSize:最大线程数

当线程数>=corePoolSize,且任务队列已满时。线程池会创建新线程来处理任务

当线程数=maxPoolSize,且任务队列已满时,线程池会拒绝处理任务而抛出异常
maxPoolSize = (max(tasks)- queueCapacity)/(1/taskcost)(最大任务数-队列容量)/每个线程每秒处理能力 = 最大线程数
计算可得 maxPoolSize = (1000-80)/10 = 92
rejectedExecutionHandler:根据具体情况来决定,任务不重要可丢弃,任务重要则要利用一些缓冲机制来处理
keepAliveTime和allowCoreThreadTimeout:采用默认通常能满足

keepAliveTime:线程空闲时间

当线程空闲时间达到keepAliveTime时,线程会退出,直到线程数量=corePoolSize 默认

allowCoreThreadTimeout:允许核心线程超时 默认

如果allowCoreThreadTimeout=true,则会直到线程数量=0

rejectedExecutionHandler:任务拒绝处理器

根据具体情况来决定,任务不重要可丢弃,任务重要则要利用一些缓冲机制来处理

两种情况会拒绝处理任务:

1):当线程数已经达到maxPoolSize,且队列已满,会拒绝新任务;

2):当线程池调用shutdown()后,会等待队列里的任务执行完毕,再关闭。如果在调用shutdown()和线程池真正关闭之间提交任务,会拒绝新任务;

线程池会调用rejectedExecutionHandler来处理这个任务。如果没有设置默认是 AbortPolicy,会抛出异常

线程池提供的拒绝策略:
ThreadPoolExecutor.AbortPolicy:丢弃任务,抛运行时异常

public static class AbortPolicy implements RejectedExecutionHandler {
	public AbortPolicy() { }

	public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
            throw new RejectedExecutionException("Task " + r.toString() +
                                                 " rejected from " +
                                                 e.toString());
        }

}

ThreadPoolExecutor.CallerRunsPolicy:将任务分给调用线程来执行,运行当前被丢弃的任务,这样做不会真的丢弃任务,但是提交的线程性能有可能急剧下降

public static class CallerRunsPolicy implements RejectedExecutionHandler {
	public CallerRunsPolicy() { }

	public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
            if (!e.isShutdown()) {
                r.run();
            }
        }
}

ThreadPoolExecutor.DiscardPolicy:忽视,什么都不会发生

public static class DiscardPolicy implements RejectedExecutionHandler {
public DiscardPolicy() { }
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
}
}

ThreadPoolExecutor.DiscardOldestPolicy:从队列中踢出最先进入队列(最后一个执行)的任务

public static class DiscardOldestPolicy implements RejectedExecutionHandler {
	public DiscardOldestPolicy() { }
	
	public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
            if (!e.isShutdown()) {
                e.getQueue().poll();
                e.execute(r);
            }
        }
	
}

三种策略都是会丢弃原有的任务。但是在某些业务场景下,我们不能够粗暴的丢弃任务。另一种拒绝策略,是通过启动线程池的线程来处理丢弃的任务,但是问题是即便是线程池空闲,它也不会执行丢弃的任务,而是等待调用线程池的主线程来执行任务,直到任务结束。
实现RejectedExecutionHandler接口,可自定义处理器

在线程池的定义中我们可以看到拒绝策略有个统一的实现接口,如下:

public interface RejectedExecutionHandler {
void rejectedExecution(Runnable r, ThreadPoolExecutor executor);
}
我们可以根据自己的业务需求来定义符合自己业务场景的处理策略。

1、Netty 中的线程池拒绝策略

 private static final class NewThreadRunsPolicy implements RejectedExecutionHandler {
        NewThreadRunsPolicy() {
            super();
        }

        public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
            try {
                final Thread t = new Thread(r, "Temporary task executor");
                t.start();
            } catch (Throwable e) {
                throw new RejectedExecutionException(
                        "Failed to start a new thread", e);
            }
        }
    }

Netty的处理方式就是不丢弃任务,这个思想和CallerRunsPolicy优点类似。只是在Netty框架中的自定义拒绝策略中,是通过新建工作线程来完成被丢弃的任务的,但是我们看一看得出它在创建线程时,没有进行条件约束,只要资源允许就不断创建新的线程来进行处理。

Dubbo 中的线程池拒绝策略

public class AbortPolicyWithReport extends ThreadPoolExecutor.AbortPolicy {

    protected static final Logger logger = LoggerFactory.getLogger(AbortPolicyWithReport.class);

    private final String threadName;

    private final URL url;

    private static volatile long lastPrintTime = 0;

    private static Semaphore guard = new Semaphore(1);

    public AbortPolicyWithReport(String threadName, URL url) {
        this.threadName = threadName;
        this.url = url;
    }

    @Override
    public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
        String msg = String.format("Thread pool is EXHAUSTED!" +
                        " Thread Name: %s, Pool Size: %d (active: %d, core: %d, max: %d, largest: %d), Task: %d (completed: %d)," +
                        " Executor status:(isShutdown:%s, isTerminated:%s, isTerminating:%s), in %s://%s:%d!",
                threadName, e.getPoolSize(), e.getActiveCount(), e.getCorePoolSize(), e.getMaximumPoolSize(), e.getLargestPoolSize(),
                e.getTaskCount(), e.getCompletedTaskCount(), e.isShutdown(), e.isTerminated(), e.isTerminating(),
                url.getProtocol(), url.getIp(), url.getPort());
        logger.warn(msg);
        dumpJStack();
        throw new RejectedExecutionException(msg);
    }

    private void dumpJStack() {
       //省略实现
    }
}

Dubbo中的自定义拒绝策略中,打印了日志,输出当前线程的堆栈信息以及执行JDK的默认拒绝策略。

自定义

public class CustomRejectionHandler implements RejectedExecutionHandler {
    @Override
    public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
        // 打印日志、暂存任务、重新执行等拒绝策略
    }
}
 /**
     * 自定义拒绝策略
     */
    publice class CustomRejectedExecutionHandler implements RejectedExecutionHandler {

        @Override
        public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
            try {
                // 核心改造点,由blockingqueue的offer改成put阻塞方法  
                executor.getQueue().put(r);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

如果任务量很大,还要求每个任务都处理成功,要对提交的任务进行阻塞提交,重写拒绝机制,改为阻塞提交。保证不抛弃一个任务

ThreadPoolExecutor执行顺序

线程池按以下行为执行任务

(1)当线程数小于核心线程数时,创建线程。

(2)当线程数大于等于核心线程数,且任务队列未满时,将任务放入任务队列。

(3)当线程数大于等于核心线程数,且任务队列已满

  a)若线程数小于最大线程数,创建线程

 b)若线程数等于最大线程数,抛出异常,拒绝任务

使用分析

针对快速响应用户请求的业务场景,我们应该从用户体验角度看,这个结果响应的越快越好,如果一个页面半天都刷不出,用户可能就放弃查看这个商品了。而面向用户的功能聚合通常非常复杂,伴随着调用与调用之间的级联、多级级联等情况,业务开发同学往往会选择使用线程池这种简单的方式,将调用封装成任务并行的执行,缩短总体响应时间。另外,使用线程池也是有考量的,这种场景最重要的就是获取最大的响应速度去满足用户,所以应该不设置队列去缓冲并发任务,调高corePoolSize和maxPoolSize去尽可能创造多的线程快速执行任务。

至于快速处理批量任务,这种场景需要执行大量的任务,我们也会希望任务执行的越快越好。这种情况下,也应该使用多线程策略,并行计算。但与响应速度优先的场景区别在于,这类场景任务量巨大,并不需要瞬时的完成,而是关注如何使用有限的资源,尽可能在单位时间内处理更多的任务,也就是吞吐量优先的问题。所以应该设置队列去缓冲并发任务,调整合适的corePoolSize去设置处理任务的线程数。在这里,设置的线程数过多可能还会引发线程上下文切换频繁的问题,也会降低处理任务的速度,降低吞吐量。

举例

import com.google.common.util.concurrent.ThreadFactoryBuilder;

import java.util.concurrent.*;
public class ThreadTask {
    public static void testHospTest() {
        // 创建一个名为shopTest-pool-%d的线程池
        ThreadFactory threadFactory = new ThreadFactoryBuilder().setNameFormat("shopTest-pool-%d").build();
        /** 构建线程池参数
         *  1.corePoolSize 核心线程数量
         *  2.maximumPoolSize 能创建的最大线程数,最大线程数不能大于核心线程数
         *  3.keepAliveTime 也就是当线程空闲时,所允许保存的最大时间,超过这个时间,线程将被释放销毁,但只针对于非核心线程
         *  4.TimeUnit 时间单位,TimeUnit.MICROSECONDS等
         *  5.workQueue 工作队列,这里有几种
         *     5.1 ArrayBlockingQueue 基于数组的有界阻塞队列,必须设置容量,遵循先进先出原则(FIFO)对元素进行排序。
         *     5.2 LinkedBlockingQueue:一个基于链表结构的阻塞队列,可以设置容量,此队列按FIFO (先进先出) 排序元素,吞吐量通常要高于ArrayBlockingQueue
         *     5.3 SynchronousQueue:一个不存储元素的阻塞队列。每个插入offer操作必须等到另一个线程调用移除poll操作,否则插入操作一直处于阻塞状态,吞吐量通常要高于LinkedBlockingQueue。
         *     5.4 PriorityBlockingQueue:一个具有优先级的无限阻塞队列。
         *  6.threadFactory 线程工厂,用于创建线程
         *  7.handler 当线程边达到最大容量时,用于处理阻塞时的程序策略
         *     7.1 ThreadPoolExecutor.AbortPolicy:丢弃任务并抛出RejectedExecutionException异常。
         *     7.2 ThreadPoolExecutor.DiscardPolicy:也是丢弃任务,但是不抛出异常。
         *     7.3 ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列最前面的任务,然后重新尝试执行任务(重复此过程)
         *     7.4 ThreadPoolExecutor.CallerRunsPolicy:由调用线程处理该任务
         *  8.executorService.execute 执行一个实现Runnable 接口的线程
         *  9.executorService.shutdown();停止线程池
         */
        // 构建线程参数
        ExecutorService executorService = new ThreadPoolExecutor(3,
                3, 0l,
                TimeUnit.MICROSECONDS,
                new LinkedBlockingQueue<>(3), threadFactory, new ThreadPoolExecutor.CallerRunsPolicy());
        try {
            int a = 20;
            // 执行线程
            executorService.execute(new Seller(a));
        } catch (Exception e) {
            System.err.println("数值小于0"+e.getMessage());
        } finally {
            // 停止线程池
            executorService.shutdown();
        }


    }
}
public class Seller implements Runnable {

    private int ticket;

    public Seller(int ticket) {
        this.ticket = ticket;
    }

    @Override
    public void run() {
        if (ticket > 0) {
            while (ticket > 0) {
                ticket--;
                System.out.println("你已经白嫖了" + ticket + "次");
            }
        } else {
            System.err.println("输入参数有误");
        }
    }
}

实战应用

需求

我们需要推送给医疗的数据大概每月月初三天内三千万条数据,但是第三方监管提供的接口只支持3000条数据推送可以估算一下,三千万条数据,一个3000条数据按3秒算,大概需要25小时左右。推送万数据后,还要核实数据,处理失败数据,

所以就考虑到引入多线程来进行并发操作,降低数据推送的时间,提高数据推送的实时性。

防止重复

我们推送给第三方的数据肯定是不能重复推送的,必须要有一个机制保证各个线程推送数据的隔离。
利用 数据库分页的方式,每个线程取 [start,limit] 区间的数据推送,我们需要保证start的一致性

失败机制

我们还得考虑到线程推送数据失败的情况。

如果是自己的系统,我们可以把多线程调用的方法抽出来加一个事务,一个线程异常,整体回滚。

但是是和第三方的对接,我们都没法做事务的,所以,我们采用了直接在数据库记录失败状态的方法,可以在后面用其它方式处理失败的数据。

线程池选择

在实际使用中,我们肯定是要用到线程池来管理线程,关于线程池,我们常用 ThreadPoolExecutor提供的线程池服务,SpringBoot中同样也提供了线程池异步的方式,虽然SprignBoot异步可能更方便一点,但是使用ThreadPoolExecutor更加直观地控制线程池,所以我们直接使用ThreadPoolExecutor构造方法创建线程池。

核心代码


@Service
public class PushProcessServiceImpl implements PushProcessService {
    @Autowired
    private PushUtil pushUtil;
    @Autowired
    private PushProcessMapper pushProcessMapper;

    private final static Logger logger = LoggerFactory.getLogger(PushProcessServiceImpl.class);

    //每个线程每次查询的条数
    private static final Integer LIMIT = 300000;
    //起的线程数
    private static final Integer THREAD_NUM = 5;
    //创建线程池
    ThreadPoolExecutor pool = new ThreadPoolExecutor(THREAD_NUM, THREAD_NUM * 2, 0, TimeUnit.SECONDS, new LinkedBlockingQueue<>(100));

    @Override
    public void pushData() throws ExecutionException, InterruptedException {
        //计数器,需要保证线程安全
        int count = 0;
        //未推送数据总数
        Integer total = pushProcessMapper.countPushRecordsByState(0);
        logger.info("未推送数据条数:{}", total);
        //计算需要多少轮
        int num = total / (LIMIT * THREAD_NUM) + 1;
        logger.info("要经过的轮数:{}", num);
        //统计总共推送成功的数据条数
        int totalSuccessCount = 0;
        for (int i = 0; i < num; i++) {
            //接收线程返回结果
            List<Future<Integer>> futureList = new ArrayList<>(32);
            //起THREAD_NUM个线程并行查询更新库,加锁
            for (int j = 0; j < THREAD_NUM; j++) {
                synchronized (PushProcessServiceImpl.class) {
                    int start = count * LIMIT;
                    count++;
                    //提交线程,用数据起始位置标识线程
                    Future<Integer> future = pool.submit(new PushDataTask(start, LIMIT, start));
                    //先不取值,防止阻塞,放进集合
                    futureList.add(future);
                }
            }
            //统计本轮推送成功数据
            for (Future f : futureList) {
                totalSuccessCount = totalSuccessCount + (int) f.get();
            }
        }
        //更新推送标志
        pushProcessMapper.updateAllState(1);
        logger.info("推送数据完成,需推送数据:{},推送成功:{}", total, totalSuccessCount);
    }

    /**
     * 推送数据线程类
     */
    class PushDataTask implements Callable<Integer> {
        int start;
        int limit;
        int threadNo;   //线程编号

        PushDataTask(int start, int limit, int threadNo) {
            this.start = start;
            this.limit = limit;
            this.threadNo = threadNo;
        }

        @Override
        public Integer call() throws Exception {
            int count = 0;
            //推送的数据
            List<PushProcess> pushProcessList = pushProcessMapper.findPushRecordsByStateLimit(0, start, limit);
            if (CollectionUtils.isEmpty(pushProcessList)) {
                return count;
            }
            logger.info("线程{}开始推送数据", threadNo);
            for (PushProcess process : pushProcessList) {
                boolean isSuccess = pushUtil.sendRecord(process);
                if (isSuccess) {   //推送成功
                    //更新推送标识
                    pushProcessMapper.updateFlagById(process.getId(), 1);
                    count++;
                } else {  //推送失败
                    pushProcessMapper.updateFlagById(process.getId(), 2);
                }
            }
            logger.info("线程{}推送成功{}条", threadNo, count);
            return count;
        }
    }
}

  • 2
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值