深入理解Java高并发编程(6) - 线程池

1. 线程池

每次为新来的任务创建线程是非常低效的,应该通过线程池的方式提供线程执行任务,做到线程的共用。

大概的模式是这样的,线程池是消费者,每次从任务队列去拿任务执行,生产者是主线程,每次产生新的任务放在任务队列中,当线程池中线程数量不能再新增,且任务队列满的时候,这时候main不能再往blocking queue里放任务,而是执行拒绝策略

image-20260312063435914

2. ThreadPoolExecutor

image-20260312070420201

  • ExecutorService:线程池最基本的接口,定义了提交任务,关闭线程池等基本方法
  • ThreadPoolThread:jdk提供的线程池最基本实现,使用int的高3位表示线程池状态,低29位表示线程数量。
    • 为什么把线程池状态和线程数量放在一个整数中:这样做的好处是只需要做一次CAS操作修改这两个值。

ThreadPoolExecutor构造方法:

public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue,
                          ThreadFactory threadFactory,
                          RejectedExecutionHandler handler) {
  • corePoolSize 核心线程数目 (最多保留的线程数)
  • maximumPoolSize 最大线程数目,减去核心线程数就是可创建的救急线程数目
  • 生存时间 - 针对救急线程
  • unit 时间单位 - 针对救急线程
  • workQueue 阻塞队列
  • threadFactory 线程工厂 - 可以为线程创建时命名
  • handler 拒绝策略

线程池中包括两种线程,两种线程开启都是懒惰创建

  • 核心线程:先创建核心线程,核心线程处理新来的任务,当核心线程正在处理任务,新来的任务会被放到任务队列(阻塞队列)中。
  • 救急线程:当核心线程达到最大核心线程数目,且任务队列满了,创建救急线程来执行任务。救急线程和核心线程最大区别是,救急线程有生存时间,当任务执行完了,经过生存时间后救急线程会被回收。 当救急线程和核心线程达到了线程池最大数目,且阻塞队列满了,最后执行拒绝策略。 无界阻塞队列不会触发救急线程创建和拒绝策略。

3. 拒绝策略

当任务队列满(只有有界队列才会存在拒绝策略),且线程池达到最大线程数(救急线程 + 核心线程都达到最大)时,线程池无法接受新的任务,此时执行拒绝策略

常见拒绝策略

  • AbortPolicy默认):直接抛出 RejectedExecutionException,拒绝任务;
  • CallerRunsPolicy:由提交任务的线程(如主线程)自己执行任务,降低并发压力;
  • DiscardPolicy:静默丢弃新任务,不抛异常;
  • DiscardOldestPolicy:丢弃队列中最旧的任务,尝试将新任务 加入队列。

image-20260312074801713

4. 线程池状态 & 关闭线程池

状态名 高 3 位 接收新任务 处理阻塞队列任务 说明
RUNNING 111 Y Y
SHUTDOWN 000 N Y 不会接收新任务,但会处理阻塞队列剩余任务
STOP 001 N N 会中断正在执行的任务,并抛弃阻塞队列任务
TIDYING 010 - - 任务全执行完毕,活动线程为 0 即将进入终结
TERMINATED 011 - - 终结状态
  • shutdown():
    • 线程池状态变为shutdown
    • 不会接受新任务
    • 已经在队列中的任务会被执行完
    • 方法不会阻塞调用线程的执行
  • shutdownNow():
    • 线程池状态变为stop
    • 不会接受新任务
    • 会把队列中任务返回
    • 会用interrupt的方式中断正在执行的任务。

5. 线程池工厂方法

Executor类下有许多创建线程池的工厂方法

  • newFixedThreadPool
    • 只有核心线程,没有救急线程,阻塞队列使用的LinkedBolckingQueue,属于无界队列,也不涉及拒绝策略
    • 适合任务量已知,耗时的任务。
  public static ExecutorService newFixedThreadPool(int nThreads) {
      return new ThreadPoolExecutor(nThreads, nThreads,
                                    0L, TimeUnit.MILLISECONDS,
                                    new LinkedBlockingQueue<Runnable>());
  }
  • newCachedThreadPool

    • 核心线程数为0,创建的都是救急线程,闲置60s后回收,救急线程可以无限创建
    • 队列采用了SynchronousQueue,无界队列,没有线程来取是放不进去的。(放取同步)
    • 适合任务数比较密集,每个任务执行时间较短的场景。
  public static ExecutorService newCachedThreadPool() {
      return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
      60L, TimeUnit.SECONDS,
      new SynchronousQueue<Runnable>());
  }
  • newSingleThreadExecutor

    • 单线程线程池,只有一个核心线程。
    • 区分单独创建一个线程执行任务
      • 单线程线程池保证永远有一个线程在工作,即使之前的工作线程因为异常停止,也会创建新的线程执行任务,而单独创建线程执行任务,异常了就被回收了
    • 和newFixedThreadPool(1)区别
      • Executor.newSingleThreadExecutor采用装饰器模式,对外只暴露ExecutorService接口,因此不能调用ThreadPoolExecutor中的方法。
      • newFixedThreadPool还可以修改最大核心线程数
  public static ExecutorService newSingleThreadExecutor(ThreadFactory threadFactory) {
        return new AutoShutdownDelegatedExecutorService
            (new ThreadPoolExecutor(1, 1,
                                    0L, TimeUnit.MILLISECONDS,
                                    new LinkedBlockingQueue<Runnable>(),
                                    threadFactory));
    }

6. 线程池里面的execute 和 submit 方法有什么区别 & 为什么线程池可以执行callable而new thread要执行FutureTask

  1. execute和submit都可以用来执行方法

    • execute用来执行runnable的方法如果方法中出现了异常,线程会终止,异常会被传递给线程池的UncaughtExceptionHandler,需要手动在任务内部捕获异常,否则异常会丢失。

    • submit既可以执行runnable的方法(执行Runnable方法时会被封装成Callable),也可以执行带返回值的callable方法当出现异常时,通过Future.get去获取返回结果会打印报错信息。

  2. 之前创建线程的时候,为什么Thread直接接受runnable的变量,如果使用callable需要先封装成futureTask。为什么线程池可以直接submit执行callable呢?

    • 线程池并不是 “直接执行 Callable”,而是帮你自动完成了 FutureTask 的创建和适配—— 你看到的 submit(Callable) 是线程池提供的 “语法糖”,底层和 Thread + FutureTask 的逻辑完全一致,只是省去了手动包装的步骤。

7. Timer & 任务调度线程池 ScheduledThreadPool

Timer(已过时):在任务调度线程池加入之前,使用Timer定时执行任务。

不使用runnable 或 callable,而是接受TimerTask执行任务,出现异常未被捕捉的时候,Timer会无法执行剩下要执行的任务。

SchuduledThreadPool,可以做到延时执行任务(延迟一段时间开始执行任务)和定时执行任务(隔一段时间执行一次)。

  • 通过Executor.newScheduledThreadPool工厂方法创建
  • 出现异常的时候,也不会影响接下来的任务运行。

8. ForkJoinThread 和 RecursiveTask

任务可以被拆分成子任务

RecursiveTask用于有返回结果的任务拆分,RecursiveAction是没有返回值的任务拆分。

通过实现在compute方法中实现任务拆分的逻辑,recursive中的fork方法是通过让一个线程去执行,join方法是获取任务的结果,通过这个两个方法来实现任务拆分逻辑。

ForkJoinPool可以去执行被拆分的子任务(RecursiveTask/RecursiveAction),使用invoke方法来执行一个rucursive的任务,ForkJoinPool通过工作窃取算法高效处理子任务

工作窃取:

  • 每个工作线程都有自己的任务队列(双端队列)
  • 线程优先执行自己队列里的任务;
  • 当自己的队列空了,会去其他线程的队列偷取任务(通常偷取队列尾部的任务);
  • 优势:减少线程间竞争,最大化利用空闲线程,提升并行效率。
posted @ 2026-04-08 16:20  不会coding的喵酱  阅读(3)  评论(0)    收藏  举报