线程池

本文最后更新于 2024年8月12日 中午

线程池是什么?

线程池就是管理一系列线程的资源池,其提供了一种限制和管理线程资源的方式。每个线程池还维护一些基本统计信息。

为什么使用线程池?

  • 降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
  • 提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。
  • 提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。

线程池的参数有哪些?

  • corePoolSize(核心线程池大小):当有新任务时,如果线程池中线程数没有达到线程池的基本大小,则会创建新的线程执行任务,否则将任务放入阻塞队列。当线程池中存活的线程数总是大于 corePoolSize 时,应该考虑调大 corePoolSize。

  • maximumPoolSize(线程池最大数量):当阻塞队列填满时,如果线程池中线程数没有超过最大线程数,则会创建新的线程运行任务。否则根据拒绝策略处理新任务。

  • keepAliveTime非核心线程空闲后,保持存活的时间,此参数只对非核心线程有效。设置为0,表示多余的空闲线程会被立即终止。

  • TimeUnit:时间单位,keepAliveTime的计量单位。

  • workQueue:工作队列,新任务被提交后,会先进入到此工作队列中,任务调度时再从队列中取出任务。

    • ArrayBlockingQueue:基于数组的有界阻塞队列,按FIFO排序。新任务进来后,会放到该队列的队尾,有界的数组可以防止资源耗尽问题。当线程池中线程数量达到corePoolSize后,再有新任务进来,则会将任务放入该队列的队尾,等待被调度。如果队列已经是满的,则创建一个新线程,如果线程数量已经达到maxPoolSize,则会执行拒绝策略。
    • LinkedBlockingQuene:基于链表的无界阻塞队列(其实最大容量为Interger.MAX),按照FIFO排序。由于该队列的近似无界性,当线程池中线程数量达到corePoolSize后,再有新任务进来,会一直存入该队列,而基本不会去创建新线程直到maxPoolSize(很难达到Interger.MAX这个数),因此使用该工作队列时,参数maxPoolSize其实是不起作用的。
    • SynchronousQuene:一个不缓存任务的阻塞队列,生产者放入一个任务必须等到消费者取出这个任务。也就是说新任务进来时,不会缓存,而是直接被调度执行该任务,如果没有可用线程,则创建新线程,如果线程数量达到maxPoolSize,则执行拒绝策略。
    • PriorityBlockingQueue:具有优先级的无界阻塞队列,优先级通过参数Comparator实现。
  • threadFactory:线程工厂,每当线程池创建一个新的线程时,都是通过线程工厂方法来完成的。在 ThreadFactory 中只定义了一个方法 newThread,每当线程池需要创建新线程就会调用它。

  • handler:当队列和线程池都满了的时候,根据拒绝策略处理新任务。

    • AbortPolicy:默认的策略,直接抛出RejectedExecutionException

    • CallerRunsPolicy:由调用线程处理该任务。

    • DiscardPolicy:不处理,直接丢弃

    • DiscardOldestPolicy:将等待队列队首的任务丢弃,并执行当前任务

说一说线程池的执行原理

线程池执行流程

  • 当线程池里存活的线程数小于核心线程数corePoolSize时,这时对于一个新提交的任务,线程池会创建一个线程去处理任务。当线程池里面存活的线程数小于等于核心线程数corePoolSize时,线程池里面的线程会一直存活着,就算空闲时间超过了keepAliveTime,线程也不会被销毁,而是一直阻塞在那里一直等待任务队列的任务来执行。
  • 当线程池里面存活的线程数已经等于corePoolSize了,这是对于一个新提交的任务,会被放进任务队列workQueue排队等待执行。
  • 当线程池里面存活的线程数已经等于corePoolSize了,并且任务队列也满了,假设maximumPoolSize>corePoolSize,这时如果再来新的任务,线程池就会继续创建新的线程来处理新的任务,直到线程数达到maximumPoolSize,就不会再创建了。
  • 如果当前的线程数达到了maximumPoolSize,并且任务队列也满了,如果还有新的任务过来,那就直接采用拒绝策略进行处理。默认的拒绝策略是抛出一个RejectedExecutionException异常。

线程池大小如何设置呢?

线程池的大小如果设置的太小,当有大量请求需要处理,系统响应比较慢,会影响用户体验。

如果线程池线程数量过大,大量线程可能会同时抢占 CPU 资源,这样会导致大量的上下文切换,从而增加线程的执行时间,影响了执行效率。

  • **CPU密集型任务(N+1)**:这种任务消耗的主要是 CPU 资源,可以将线程数设置为 N(CPU 核心数)+1,多出来的一个线程是为了防止某些原因导致的线程阻塞而带来的影响。一旦某个线程被阻塞,释放了CPU资源,而在这种情况下多出来的一个线程就可以充分利用 CPU 的空闲时间。
  • **I/O密集型任务(2N)**:系统的大部分时间都在处理 IO 操作,此时线程可能会被阻塞,释放CPU资源,这时就可以将 CPU 交出给其它线程使用。

线程池的类型有哪些?

  • FixedThreadPool:固定线程数的线程池。任何时间点,最多只有 nThreads 个线程处于活动状态执行任务。
    • 使用无界队列LinkedBlockingQueue
    • 运行中的线程池不会拒绝任务
    • maxThreadPoolSize 是无效参数,故将它的值设置为与 coreThreadPoolSize 一致。
    • keepAliveTime 也是无效参数,设置为0L,因为此线程池里所有线程都是核心线程,默认情况下核心线程不会被回收
    • 适用于处理CPU密集型的任务,确保CPU在长期被工作线程使用的情况下,尽可能的少的分配线程,即适用执行长期的任务。
  • SinghleThreadExecutor:只有一个线程的线程池。
    • 使用无界队列 LinkedBlockingQueue。
    • 线程池只有一个运行的线程,新来的任务放入工作队列,线程处理完任务就循环从队列里获取任务执行。保证顺序的执行各个任务。
    • 适用于串行执行任务的场景,一个任务一个任务地执行。
  • CachedThreadPool:根据需要创建新线程的线程池。
    • 使用没有容量的SynchronousQueue作为线程池工作队列
    • 主线程提交任务的速度高于线程处理任务的速度时,CachedThreadPool 会不断创建新的线程。极端情况下,这样会导致耗尽 cpu 和内存资源。
    • 用于并发执行大量短期的小任务。CachedThreadPool允许创建的线程数量为 Integer.MAX_VALUE
  • ScheduledThreadPoolExecutor:在给定的延迟后运行任务,或者定期执行任务。

如何判断线程池的任务是不是执行完了?

  • 使用线程池的原生函数isTerminated();如果全部完成返回true,否则返回false。
  • 使用重入锁,维持一个公共计数。所有的普通任务维持一个计数器,当任务完成时计数器加一(这里要加锁),当计数器的值等于任务数时,这时所有的任务已经执行完毕了。
  • 使用CountDownLatch。它的原理跟第二种方法类似,给CountDownLatch一个计数值,任务执行完毕后,调用countDown()执行计数值减一。最后执行的任务在调用方法的开始调用await()方法,这样整个任务会阻塞,直到这个计数值为零,才会继续执行。缺点就是需要提前知道任务的数量。
  • submit向线程池提交任务,使用Future判断任务执行状态。使用submit向线程池提交任务与execute提交不同,submit会有Future类型的返回值。通过future.isDone()方法可以知道任务是否执行完成。

execute和submit的区别是什么?

  • execute只能提交Runnable类型的任务,无返回值。submit既可以提交Runnable类型的任务,也可以提交Callable类型的任务,会有一个类型为Future的返回值,但当任务类型为Runnable时,返回值为null。
  • execute在执行任务时,如果遇到异常会直接抛出,而submit不会直接抛出,只有在使用Future的get方法获取返回值时,才会抛出异常。
  • execute所属顶层接口是Executor,submit所属顶层接口是ExecutorService,实现类ThreadPoolExecutor重写了execute方法,抽象类AbstractExecutorService重写了submit方法。

线程池
https://love-enough.github.io/2024/08/12/线程池/
作者
GuoZihan
发布于
2024年8月12日
更新于
2024年8月12日
许可协议