Java 并发之ThreadPoolExecutor线程池

718 阅读7分钟

前言

何谓线程池

  • 一种线程使用模式。线程过多会带来调度开销,进而影响缓存局部性和整体性能。而线程池维护着多个线程,等待着分配可并发执行的任务。这避免了在处理短时间任务时创建与销毁线程的代价。
  • 线程池不仅能够保证CPU内核的充分利用,还能防止过分调度。可用线程数量应该取决于可用的并发处理器、处理器内核、内存、网络sockets等的数量。 例如,线程数一般取cpu数量+2比较合适,线程数过多会导致额外的线程切换开销。
  • 即线程池的线程数不是越多好。

线程池的应用范围

  • 需要大量的线程来完成任务,且完成任务的时间比较短。 WEB服务器完成网页请求这样的任务,使用线程池技术是非常合适的。因为单个任务小,而任务数量巨大。
  • 但对于长时间的任务,线程池的优点就不明显了。因为线程创建后不能复用,会导致大量任务阻塞,没有线程可以执行它。
  • 对性能要求苛刻的应用,比如要求服务器迅速响应客户请求。
  • 接受突发性的大量请求,但不至于使服务器因此产生大量线程的应用。突发性大量客户请求,在没有线程池情况下,将产生大量线程,虽然理论上大部分操作系统线程数目最大值不是问题,短时间内产生大量线程可能使内存到达极限,并出现"内存溢出(OOM)"的错误。

Java 线程池

线程池起源于Executor接口

  • Executor接口其中一个分支,如下图:

image.png

image.png

image.png

image.png

线程池的使用

代码样例

public class ExecutorTest2 {
    public static void main(String[] args) {
        ExecutorService executorService = new ThreadPoolExecutor(8,10,0, TimeUnit.SECONDS,
                new LinkedBlockingDeque<>(5), new ThreadPoolExecutor.AbortPolicy());//创建线程池
        Runnable runnable = ()-> {//runable 任务的内容
            try {
                //System.out.println(executorService);
                Thread.sleep(1000);
                System.out.println(Thread.currentThread().getName());

            } catch (Exception e) {
                e.printStackTrace();
            }
        };
        IntStream.range(0,9).forEach(i-> executorService.submit(runnable));//像线程池提交任务,提交9个
        executorService.shutdown();
    }
}

创建线程池

有一个Executors类,它有工厂方法,可以用来创建线程池,但在实际应用中并不使用,不赘述。

ThreadPoolExecutor的构造方法

image.png

参数解释

参数:

  • corePoolSize – 要保留在池中的核心线程数,即使它们处于空闲状态,除非设置了allowCoreThreadTimeOut
  • maximumPoolSize – 池中允许的最大线程数
  • keepAliveTime – 当线程数大于核心数时,这是多余空闲线程在终止前等待新任务的最长时间。
  • unit – keepAliveTime参数的时间单位
  • workQueue – 用于在执行任务之前保存任务的队列。 这个队列将只保存execute方法提交的Runnable任务。
  • threadFactory – 执行程序创建新线程时使用的工厂
  • handler – 任务无法放进线程池时使用的处理程序,因为达到了线程maximumPoolSize和队列(workQueue)容量 抛出: IllegalArgumentException
  • 如果以下情况之一成立:
    • corePoolSize < 0
      keepAliveTime < 0
      maximumPoolSize <= 0
      maximumPoolSize < corePoolSize NullPointerException
  • 如果workQueue或threadFactory或handler为 null

关于线程池的抽象和执行的流程

  • 抽象 image.png
  • 流程

image.png 关于线程池的总体执行策略:

  1. 如果线程池中正在执行的线程数 < corePoolSize,那么线程池就会优先选择创建新的线程而非将提交的任务加到阻塞队列(workQueue)中。
  2. 如果线程池中正在执行的线程数 >= corePoolSize,那么线程池就会优先选择对提交的任务进行阻塞排队而非创建新的线程。
  3. 如果提交的任务无法加入到阻塞队列当中,那么线程池就会创建新的线程;如果创建的线程数超过了maximumPoolSize,那么拒绝策略(handler)就会起作用。

又到了喜闻乐见的阅读jdk源码环节

拒绝策略

  • AbortPolicy

image.png

  • DiscardPolicy

image.png

  • DiscardOldestPolicy image.png
  • CallerRunsPolicy

image.png

其实,一般应用中也不用这些jdk自带的拒绝策略
一般由程序员根据实际情况实现RejectedExecutionHandler接口

submit方法

image.png

  • submit的机制

image.png

关于线程池任务提交的总结:

  • 两种提交方式: submit与execute。

  • submit有三种方式,无论哪种方式,最终都是将传递进来的任务转换为一个callable对象进行处理。

    • 统一任务接口:将所有任务都封装为Callable对象,使得线程池对任务的处理方式一致。这样,无论是执行有返回结果的任务还是无返回结果的任务,线程池可以使用相同的调度、管理和异常处理机制。这种一致性使得线程池更易于使用和管理。
  • 当callable对象构造完毕后,最终都会调用Executor接口中声明的execute方法进行统一的处理。

线程池的状态

线程池一共存在5种状态:

  • RUNNING︰线程池可以接收新的任务提交,并且还可以正常处理阻塞队列中的任务。
  • SHUTDOWN︰不再接收新的任务提交,不过线程池可以继续处理阻塞队列中的任务。
  • STOP:不再接收新的任务,同时还会丢弃阻塞队列中的既有任务﹔此外,它还会中断正在处理中的任务。
  • TIDYING:所有的任务都执行完毕后(同时也涵盖了阻塞队列中的任务),当前线程池中的活动的线程数量降为0,将会调用terminated方法。
  • TERMINATED︰线程池的终止状态,当terminated方法执行完毕后,线程池将会处于该状态之下。

线程池状态的变化并不是连续的,它会视情况转换到相应的状态。并不一定是从RUNNING到SHUTDOWN。
RUNNING -> SHUPDOWN∶当调用了线程池的shutdown方法时,或者当finalize方法被隐式调用后(该方法内部会调用shutdown方法)。
RUNNING,SHUTDOWN -> STOP:当调用了线程池的shutdownNow方法时。
SHUTDOWN ->TIDYING︰在线程池与阻塞队列均变为空时。
STOP ->TIDYING:在线程池变为空时。
TIDYING ->TERMINATED:在terminated方法被执行完毕时。

状态源自jdk源码

image.png

如此就可以阅读jdk获知execute方法的实现

image.png

关于线程池状态是如何shutdown的,jdk源码后续阅读

线程复用的关键

image.png

阻塞队列

  • 阻塞队列(BlockingQueue)是一个线程安全的队列,支持在队列为空时阻塞出队操作,以及在队列已满时阻塞入队操作。在Java中,阻塞队列是通过接口的方式定义的,主要包括以下几个实现类:ArrayBlockingQueue、LinkedBlockingQueue、SynchronousQueue、PriorityBlockingQueue等。

  • 下面以ArrayBlockingQueue为例,简要介绍一下阻塞队列的原理及源码解释。 image.png image.png

    • ArrayBlockingQueue是一种有界的阻塞队列,它的内部维护了一个定长的数组作为存储容器。当队列为空时,出队操作会被阻塞,直到队列中有新的元素;当队列已满时,入队操作会被阻塞,直到队列中有空闲位置。
    • 具体实现方式是,ArrayBlockingQueue内部使用了两个锁(putLock和takeLock)以及两个条件变量(notEmpty和notFull)来实现线程间的同步和通信。当队列已满时,put()方法会获取putLock锁,然后在notFull条件变量上等待,直到队列有空闲位置;当队列为空时,take()方法会获取takeLock锁,然后在notEmpty条件变量上等待,直到队列中有新的元素。当有元素入队或出队时,相应的条件变量会被唤醒,从而通知等待的线程进行操作。
    • 整个模型就是基于生产者-消费者模型

为什么要用阻塞队列

  • 普通的线程安全队列的确可以提供一些基本的功能,如任务的缓冲、任务排队和任务传递等。但是,与阻塞队列相比,它们在某些方面可能存在一些限制:
    1. 缺少阻塞特性:普通的线程安全队列通常采用非阻塞的方式处理元素的插入和移除。这意味着当队列为空时,消费者线程将会持续尝试获取元素,可能导致高额的CPU消耗。当队列已满时,生产者线程将会持续尝试插入元素,也可能导致高额的CPU消耗。这种忙等待的情况可能会浪费系统资源。

      • 阻塞时不会占用cpu时间片,线程会被挂起。
    2. 无法限制队列容量:普通的线程安全队列通常没有容量限制,可以无限制地接受元素。这可能导致资源过度占用,特别是在高负载情况下,当生产者速度远远超过消费者速度时。

    3. 无法提供任务优先级:一些阻塞队列实现(如PriorityBlockingQueue)允许为任务指定优先级,从而影响任务的执行顺序。普通的线程安全队列通常不支持这种优先级的概念。