线程池实现原理
蘑菇街面试,设计一个线程池
并发队列
入队
非阻塞队列:当队列中满了时候,放入数据,数据丢失
阻塞队列:当队列满了的时候,进行等待,什么时候队列中有出队的数据,那么第11个再放进去
出队
非阻塞队列:如果现在队列中没有元素,取元素,得到的是null
阻塞队列:等待,什么时候放进去,再取出来
线程池使用的是阻塞队列
线程池概念
线程是稀缺资源,如果被无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,合理的使用线程池对线程进行统一分配、调优和监控,有以下好处:
- 降低资源消耗;
- 提高响应速度;
- 提高线程的可管理性。
Java1.5 中引入的 Executor 框架把任务的提交和执行进行解耦,只需要定义好任务,然后提交给线程池,而不用关心该任务是如何执行、被哪个线程执行,以及什么时候执行。
Executor类图
线程池工作原理
线程池中的核心线程数,当提交一个任务时,线程池创建一个新线程执行任务,直到当前线程数等于corePoolSize;如果当前线程数为 corePoolSize,继续提交的任务被保存到阻塞队列中,等待被执行;如果阻塞队列满了,那就创建新的线程执行当前任务;直到线程池中的线程数达到 maxPoolSize,这时再有任务来,只能执行 reject() 处理该任务。
初始化线程池
- newFixedThreadPool() 说明:初始化一个指定线程数的线程池,其中 corePoolSize == maxiPoolSize,使用 LinkedBlockingQuene 作为阻塞队列 特点:即使当线程池没有可执行任务时,也不会释放线程。
- newCachedThreadPool() 说明:初始化一个可以缓存线程的线程池,默认缓存60s,线程池的线程数可达到 Integer.MAX_VALUE,即 2147483647,内部使用 SynchronousQueue 作为阻塞队列; 特点:在没有任务执行时,当线程的空闲时间超过 keepAliveTime,会自动释放线程资源;当提交新任务时,如果没有空闲线程,则创建新线程执行任务,会导致一定的系统开销; 因此,使用时要注意控制并发的任务数,防止因创建大量的线程导致而降低性能。
- newSingleThreadExecutor() 说明:初始化只有一个线程的线程池,内部使用 LinkedBlockingQueue 作为阻塞队列。 特点:如果该线程异常结束,会重新创建一个新的线程继续执行任务,唯一的线程可以保证所提交任务的顺序执行
- newScheduledThreadPool() 特点:初始化的线程池可以在指定的时间内周期性的执行所提交的任务,在实际的业务场景中可以使用该线程池定期的同步数据。
初始化方法
// 使用Executors静态方法进行初始化 ExecutorService service = Executors.newSingleThreadExecutor(); // 常用方法 service.execute(new Thread()); service.submit(new Thread()); service.shutDown(); service.shutDownNow();
常用方法
execute与submit的区别
- 接收的参数不一样
- submit有返回值,而execute没有
用到返回值的例子,比如说我有很多个做 validation 的 task,我希望所有的 task 执行完,然后每个 task 告诉我它的执行结果,是成功还是失败,如果是失败,原因是什么。然后我就可以把所有失败的原因综合起来发给调用者。
- submit方便Exception处理
如果你在你的 task 里会抛出 checked 或者 unchecked exception,而你又希望外面的调用者能够感知这些 exception 并做出及时的处理,那么就需要用到 submit,通过捕获 Future.get 抛出的异常。
shutDown与shutDownNow的区别
当线程池调用该方法时,线程池的状态则立刻变成 SHUTDOWN 状态。此时,则不能再往线程池中添加任何任务,否则将会抛出 RejectedExecutionException 异常。但是,此时线程池不会立刻退出,直到添加到线程池中的任务都已经处理完成,才会退出。
内部实现
public ThreadPoolExecutor( int corePoolSize, // 核心线程数 int maximumPoolSize, // 最大线程数 long keepAliveTime, // 线程存活时间(在 corePore<*<maxPoolSize 情况下有用) TimeUnit unit, // 存活时间的时间单位 BlockingQueue<Runnable> workQueue // 阻塞队列(用来保存等待被执行的任务) ThreadFactory threadFactory, // 线程工厂,主要用来创建线程; RejectedExecutionHandler handler // 当拒绝处理任务时的策略 ){ this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, Executors.defaultThreadFactory(), defaultHandler); }
关于 workQueue 参数,有四种队列可供选择:
- ArrayBlockingQueue:基于数组结构的有界阻塞队列,按 FIFO 排序任务;
- LinkedBlockingQuene:基于链表结构的阻塞队列,按 FIFO 排序任务;
- SynchronousQuene:一个不存储元素的阻塞队列,每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态,吞吐量通常要高于 ArrayBlockingQuene;
- PriorityBlockingQuene:具有优先级的无界阻塞队列;
关于 handler 参数,线程池的饱和策略,当阻塞队列满了,且没有空闲的工作线程,如果继续提交任务,必须采取一种策略处理该任务,线程池提供了 4 种策略:
- ThreadPoolExecutor.AbortPolicy:丢弃任务并抛出RejectedExecutionException异常。
- ThreadPoolExecutor.DiscardPolicy:丢弃任务,但是不抛出异常。
- ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列最前面的任务,然后重新尝试执行任务(重复此过程)
- ThreadPoolExecutor.CallerRunsPolicy:由调用线程处理该任务
当然也可以根据应用场景实现 RejectedExecutionHandler 接口,自定义饱和策略,如记录日志或持久化存储不能处理的任务。
线程池的状态
private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
其中 AtomicInteger 变量 ctl 的功能非常强大:利用低 29 位表示线程池中线程数,通过高 3 位表示线程池的运行状态:
- RUNNING:-1 << COUNT_BITS,即高 3 位为 111,该状态的线程池会接收新任务,并处理阻塞队列中的任务;
- SHUTDOWN: 0 << COUNT_BITS,即高 3 位为 000,该状态的线程池不会接收新任务,但会处理阻塞队列中的任务;
- STOP : 1 << COUNT_BITS,即高 3 位为 001,该状态的线程不会接收新任务,也不会处理阻塞队列中的任务,而且会中断正在运行的任务;
- TIDYING : 2 << COUNT_BITS,即高 3 位为 010,该状态表示线程池对线程进行整理优化;
- TERMINATED: 3 << COUNT_BITS,即高 3 位为 011,该状态表示线程池停止工作;
线程池其他常用方法
如果执行了线程池的 prestartAllCoreThreads() 方法,线程池会提前创建并启动所有核心线程。 ThreadPoolExecutor 提供了动态调整线程池容量大小的方法:setCorePoolSize() 和 setMaximumPoolSize()。
如何合理设置线程池的大小
一般需要根据任务的类型来配置线程池大小: 如果是 CPU 密集型任务,就需要尽量压榨 CPU,参考值可以设为 NCPU+1 如果是 IO 密集型任务,参考值可以设置为 2*NCPU



浙公网安备 33010602011771号