线程池是我们在工作中处理大量数据时经常会使用到的知识,这篇文章并不会去对线程池的具体知识进行学习,而是针对JDK、Tomcat和Dubbo的线程池进行一个学习,通过这篇文章,可以了解到IO密集型和CPU密集型的线程池的设计。
JDK线程池
JDK线程池是jdk原生的线程池,其主要有核心线程数、最大线程数、线程存活时间、时间单位、阻塞队列、线程工厂、拒绝策略这几个参数。
这里并不会对线程池的实现做具体的分析,而是从这几个线程池的使用场景来看下为什么这么设计。在JDK线程池中,当核心线程的数量到达规定数量,就会尝试将任务线程加入到阻塞队列中,只有阻塞队列满时才会继续创建线程,以此来保证线程池的处理效率。
我们可以分析出来,JDK线程池会优先使用CPU数的核心线程数来处理任务线程,这样可以将CPU的资源利用到最大,而只有当阻塞队列满的时候才会创建超过核心线程数的普通线程,来加快处理任务线程。所以,JDK线程池其是偏向于CPU密集型的线程池设计。
Tomcat线程池是tomcat在jdk原生线程池的基础上,tomcat的线程池主要是查看StandardThreadExecutor类,在此类中主要关注的方法是startInternal()。此方法是容器启动时会加载的。 在这个方法中主要是初始化了tomcat自己实现的TaskQueue,线程工厂和tomcat实现的ThreadPoolExecutor类实体。并将ThreadPoolExecutor类对象设为TaskQueue的parent变量。
tomcat自己实现的TaskQueue类主要是调整了offer方法,其offer方法中 在线程池对象为null || 线程池的当前线程数量 == 线程池的最大线程数量 || 以提交的线程数量 < 线程池的当前线程数量 时,会尝试将线程加入到阻塞队列中。而当线程池的当前线程
数量 < 线程池的最大数量时,会抛出异常RejectedExecutionException异常,去创建新的线程,这样写的方式是为了最大程度的将任务写入队列
。
tomcat多数执行的是IO操作,所以其这种设计方式是偏向于IO密集型的线程池设计。
我们先来看一下老版本的dubbo中consumer端的线程池设计:
1.当consumer接受到业务请求之后,会从I/O线程池中拿到一个Future实例
2.业务线程调用future.get来阻塞的等待结果的返回
3.业务数据返回之后,consumer端采用独立的序列化线程池来进行反序列化处理,并通过future.set将反序列化后的结果置回
在老版本的dubbo中,是有可能出现consumer端因为线程数分配过多而导致OOM的问题出现,具体情况查看此链接Need a limited Threadpool in consumer side #2013
为了解决这种问题,通过复用业务端被阻塞的线程,来解决这个问题。来看下新版本中consumer端的线程池设计:
1.当consumer接受到业务请求之后,会从I/O线程池中拿到一个Future实例
2.在调用future.get之前,先调用ThreadlessExecutor.wait,wait会使业务线程在阻塞队列上等待,直到队列中被加入元素。
3.当业务数据返回后,生成一个Runable Task放入ThreadlessExecutor队列
4.业务线程将Task取出,并放到自己线程中执行:反序列化数据并set到Future中
这种方式采用业务线程自己监测并解析返回结果,减少了额外的线程池开销