跳至主要內容

10、线程池

KindBrave大约 5 分钟

1.什么是线程池/优点

线程池就是利用池化技术来管理线程。使用线程池的优点:

  • 降低资源消耗。线程池可以规定线程数量,并且可以复用线程。
  • 提高响应速度。因为线程池当中的线程是可以复用的,所以可以免去线程创建和销毁所带来的时间消耗
  • 方便进行线程管理。线程池中的线程是可监控可管理的。

2.创建线程池的方式

  • 使用工厂类Executors
  • 使用ThreadPoolExecutor

⭐Executors

不推荐使用这个工厂类。因为它内部的很多线程池可能会导致OOM(比如FixedThreadPool、SingleThreadExecutor,使用无限队列,可能导致队列OOM。CachedThreadPool,使用无限线程,可能导致OOM),而且不方便进行灵活控制。

Executors中的线程池介绍:

  • FixedThreadPool。它的核心线程数量和最大线程数量相同,阻塞队列使用LinkedBlockingQueue,即阻塞队列无限大,可能导致OOM
  • SingleThreadExecutor。核心线程数和最大线程数都是1,阻塞队列同上,可能导致OOM
  • CachedThreadPool。核心线程数为0,最大线程数为无限大,阻塞队列为SynchronousQueue同步队列,即阻塞队列不可存放作业,可能导致OOM
  • ScheduledThreadPool。核心线程数自定,最大线程数为无限大,阻塞队列为DelayedWorkQueue延时队列(这个延时是指任务延时),可能导致OOM。

因为它们都会导致OOM,所以不推荐。

⭐ThreadPoolExecutor

推荐使用ThreadPoolExecutor来进行灵活控制线程池

2.1 构造参数

ThreadPoolExecutor的构造参数有哪些?

  • 核心线程数:线程池的核心线程数,如果当前运行的线程数量小于这个数,那么会创建新线程来执行任务
  • 最大线程数:线程池可以运行的最大线程数
  • 空闲线程等待时间:如果线程空闲时间超过这个时间,那么会进行销毁
  • 时间单位:上述的时间单位
  • 线程池饱和策略:如果线程数量达到最大,任务队列也满了,那么会执行这个策略。

2.2 饱和策略

  • 线程池饱和策略有哪些?

    • 拒绝。直接抛出异常
    • 调用者执行。通过调用者所在的线程来执行。这个会让调用者所在线程执行变慢,如果不在意可以考虑。因为这个不会抛弃任何任务。
    • 丢弃。新的任务直接丢弃,不处理
    • 丢弃最早。把消息队列中最早的任务丢掉,放入本个任务。

2.3 阻塞队列

  • 阻塞队列有哪些?
    • 有限队列。ArrayBlockingQueue。该队列有界限,底层数据结构是ArrayList。
    • 无限队列。LinkedBlockingQueue。该队列默认长度为Inteter.MAX_VALUE(可手动指定有界),可以算作无界,底层用的是LinkedList。该队列用在了SingleThreadExecutor和FixedThreadPool中。其中SingleThreadExecutor的核心线程和最大线程都是1,FixedThreadPool的核心线程和最大线程是一致的。它们的任务队列可以看作无界的。
    • 同步队列。同步队列的空间为0,不能存放任何任务。它用在CachedThreadPool中,CachedThreadPool的最大线程数为无限。
    • 延迟队列。延迟队列底层使用堆来实现的。延迟队列排序方式不是任务的添加顺序,而是任务的延迟时间每次出队的任务都是当前延迟最小的任务
  • LinkedBlockingQueue和ArrayBlockingQueue的区别?
    • L默认是无界队列,默认长度是Integer.MAX_VALUE,而A在初始化的时候必须指定容量,是有界队列
    • 锁机制不同,A没有进行锁分离,即入队和出队用的是一把ReentrantLock,而L是锁分离的,入队和出队用的是两把锁。也因此,L的并发性能会更好一些
    • 底层实现不同,A底层是数组,L底层是链表

3. 线程池处理任务细节

线程池处理新来的任务有以下几个步骤:

  • 如果当前线程池中运行的线程数量小于核心线程数,那么会创建新线程来执行
  • 如果当前线程池中运行的线程数量大于等于核心线程数,但是任务队列还没有满,那么先放到任务队列中等待执行
  • 如果任务队列也满了,那么会判断当前运行的线程数量是否大于最大线程,如果不大于,那么会继续创建线程来执行
  • 如果当前运行的线程已经到达最大线程数量了,那么就拒绝调用饱和策略了。

4. 线程池是如何进行线程复用的

我们似乎并没有在上述线程池处理细节中发现如何进行线程复用的。实际上的奥秘在于Worker中。

我们知道,如果当前运行的线程数量不大于核心线程数,会addWorker。在大于核心线程数,会放到任务队列当中去,那么最终这个任务队列当中的任务是谁执行的呢?

没错,就是之前addWorker中的Worker。这些Worker不会在执行完一个任务后就释放掉,而是会阻塞等待任务队列中的任务,如果有任务来了,就会交给它们来执行。